This is an automated email from the ASF dual-hosted git repository.

ricardozanini pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new 448d767495d kie-issues#1549: kn-workflow-plugin check for the presence 
of an image in the local Docker image does not cover all cases (#2685)
448d767495d is described below

commit 448d767495d268ddd7f1185e4e5eec3833fa9b21
Author: Dmitrii Tikhomirov <[email protected]>
AuthorDate: Tue Oct 29 12:07:13 2024 -0700

    kie-issues#1549: kn-workflow-plugin check for the presence of an image in 
the local Docker image does not cover all cases (#2685)
---
 packages/kn-plugin-workflow/go.mod                 |  3 +-
 packages/kn-plugin-workflow/go.sum                 |  1 +
 .../kn-plugin-workflow/pkg/common/containers.go    | 95 ++++++++++++++++------
 .../pkg/common/containers_test.go                  | 75 +++++++++++++++++
 4 files changed, 147 insertions(+), 27 deletions(-)

diff --git a/packages/kn-plugin-workflow/go.mod 
b/packages/kn-plugin-workflow/go.mod
index 35e3663862a..b8dfab167a6 100644
--- a/packages/kn-plugin-workflow/go.mod
+++ b/packages/kn-plugin-workflow/go.mod
@@ -12,6 +12,7 @@ require (
        github.com/apache/incubator-kie-tools/packages/sonataflow-operator/api 
v0.0.0
        
github.com/apache/incubator-kie-tools/packages/sonataflow-operator/workflowproj 
v0.0.0
        github.com/beevik/etree v1.2.0
+       github.com/docker/distribution v2.8.2+incompatible
        github.com/docker/docker v24.0.9+incompatible
        github.com/docker/go-connections v0.4.0
        github.com/jstemmer/go-junit-report/v2 v2.0.0
@@ -31,7 +32,6 @@ require (
        github.com/cespare/xxhash/v2 v2.2.0 // indirect
        github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 
indirect
        github.com/dgraph-io/ristretto v0.1.1 // indirect
-       github.com/docker/distribution v2.8.2+incompatible // indirect
        github.com/docker/go-units v0.5.0 // indirect
        github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // 
indirect
        github.com/dustin/go-humanize v1.0.1 // indirect
@@ -88,6 +88,7 @@ require (
        github.com/spf13/cast v1.5.1 // indirect
        github.com/spf13/jwalterweatherman v1.1.0 // indirect
        github.com/spf13/pflag v1.0.5 // indirect
+       github.com/stretchr/objx v0.5.0 // indirect
        github.com/subosito/gotenv v1.6.0 // indirect
        github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
        golang.org/x/crypto v0.21.0 // indirect
diff --git a/packages/kn-plugin-workflow/go.sum 
b/packages/kn-plugin-workflow/go.sum
index f4a9214d352..7c2d83ca084 100644
--- a/packages/kn-plugin-workflow/go.sum
+++ b/packages/kn-plugin-workflow/go.sum
@@ -347,6 +347,7 @@ github.com/spf13/pflag v1.0.5 
h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod 
h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod 
h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod 
h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod 
h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod 
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
diff --git a/packages/kn-plugin-workflow/pkg/common/containers.go 
b/packages/kn-plugin-workflow/pkg/common/containers.go
index af06f6a68a9..afb2482b69e 100644
--- a/packages/kn-plugin-workflow/pkg/common/containers.go
+++ b/packages/kn-plugin-workflow/pkg/common/containers.go
@@ -20,10 +20,13 @@
 package common
 
 import (
+       "bufio"
+       "bytes"
        "context"
        "encoding/json"
        "errors"
        "fmt"
+       "github.com/docker/distribution/reference"
        "io"
        "os"
        "os/exec"
@@ -31,11 +34,11 @@ import (
        "runtime"
        "strings"
        "syscall"
+       "time"
 
        
"github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/metadata"
        "github.com/docker/docker/api/types"
        "github.com/docker/docker/api/types/container"
-       "github.com/docker/docker/api/types/filters"
        "github.com/docker/docker/client"
        "github.com/docker/docker/pkg/stdcopy"
        "github.com/docker/go-connections/nat"
@@ -51,6 +54,10 @@ type DockerLogMessage struct {
        ID     string `json:"id,omitempty"`
 }
 
+type DockerClient interface {
+       ImageList(ctx context.Context, options types.ImageListOptions) 
([]types.ImageSummary, error)
+}
+
 func getDockerClient() (*client.Client, error) {
        cli, err := client.NewClientWithOpts(client.FromEnv, 
client.WithAPIVersionNegotiation())
        if err != nil {
@@ -198,29 +205,71 @@ func 
GracefullyStopTheContainerWhenInterrupted(containerTool string) {
        }()
 }
 
-func pullDockerImage(cli *client.Client, ctx context.Context) (io.ReadCloser, 
error) {
+func pullDockerImage(cli *client.Client, ctx context.Context) error {
        // Check if the image exists locally.
-       // For that we should check only the image name and tag, removing the 
registry,
-       // as `docker image ls --filter reference=<image_full_url>` will return 
empty if the image_full_url is not the first tag
-       // of an image.
-       imageNameWithoutRegistry := strings.Split(metadata.DevModeImage, "/")
-       imageFilters := filters.NewArgs()
-       imageFilters.Add("reference", fmt.Sprintf("*/%s", 
imageNameWithoutRegistry[len(imageNameWithoutRegistry)-1]))
-       images, err := cli.ImageList(ctx, types.ImageListOptions{Filters: 
imageFilters})
+       exists, err := CheckImageExists(cli, ctx, metadata.DevModeImage)
        if err != nil {
-               return nil, fmt.Errorf("error listing images: %s", err)
+               return fmt.Errorf("error listing images: %s", err)
        }
 
        // If the image is not found locally, pull it from the remote registry
-       if len(images) == 0 {
-               reader, err := cli.ImagePull(ctx, metadata.DevModeImage, 
types.ImagePullOptions{})
-               if err != nil {
-                       return nil, fmt.Errorf("\nError pulling image: %s. 
Error is: %s", metadata.DevModeImage, err)
+       if !exists {
+               fmt.Printf("\n⏳ Retrieving (%s), this could take some 
time...\n", metadata.DevModeImage)
+
+               ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
+               defer cancel()
+
+               reader, writer := io.Pipe()
+               defer writer.Close()
+
+               var stderr bytes.Buffer
+
+               go func() {
+                       scanner := bufio.NewScanner(reader)
+                       for scanner.Scan() {
+                               fmt.Print(".")
+                       }
+               }()
+
+               // we use local docker client to pull the image
+               cmd := exec.CommandContext(ctx, "docker", "pull", 
metadata.DevModeImage)
+               cmd.Stdout = writer
+               cmd.Stderr = &stderr
+
+               if err := cmd.Start(); err != nil {
+                       return fmt.Errorf("\nError pulling image: %s. Error is: 
%s", metadata.DevModeImage, err)
+               }
+
+               if err := cmd.Wait(); err != nil {
+                       return fmt.Errorf("\nError pulling image: %s. Error is: 
%s", metadata.DevModeImage, stderr.String())
                }
-               return reader, nil
+               fmt.Println("\n🎉 Successfully pulled the image")
+       }
+
+       return nil
+}
+
+func CheckImageExists(cli DockerClient, ctx context.Context, imageName string) 
(bool, error) {
+       named, err := reference.ParseNormalizedNamed(imageName)
+
+       if tagged, ok := named.(reference.Tagged); ok {
+               imageName = fmt.Sprintf("%s:%s", reference.Path(named), 
tagged.Tag())
+       } else {
+               imageName = fmt.Sprintf("%s:%s", reference.Path(named), 
"latest")
+       }
+       images, err := cli.ImageList(ctx, types.ImageListOptions{All: true})
+       if err != nil {
+               return false, fmt.Errorf("error listing images: %s", err)
        }
 
-       return nil, nil
+       for _, image := range images {
+               for _, tag := range image.RepoTags {
+                       if strings.HasSuffix(tag, imageName) {
+                               return true, nil
+                       }
+               }
+       }
+       return false, nil
 }
 
 func processDockerImagePullLogs(reader io.ReadCloser) error {
@@ -286,24 +335,18 @@ func startDockerContainer(cli *client.Client, ctx 
context.Context, resp containe
 }
 
 func runDockerContainer(portMapping string, path string) error {
-       ctx := context.Background()
+       ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
+       defer cancel()
+
        cli, err := getDockerClient()
        if err != nil {
                return err
        }
-
-       reader, err := pullDockerImage(cli, ctx)
+       err = pullDockerImage(cli, ctx)
        if err != nil {
                return err
        }
 
-       if reader != nil {
-               fmt.Printf("\n⏳ Retrieving (%s), this could take some 
time...\n", metadata.DevModeImage)
-               if err := processDockerImagePullLogs(reader); err != nil {
-                       return err
-               }
-       }
-
        resp, err := createDockerContainer(cli, ctx, portMapping, path)
        if err != nil {
                return err
diff --git a/packages/kn-plugin-workflow/pkg/common/containers_test.go 
b/packages/kn-plugin-workflow/pkg/common/containers_test.go
new file mode 100644
index 00000000000..c54cfc7f6c6
--- /dev/null
+++ b/packages/kn-plugin-workflow/pkg/common/containers_test.go
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 common
+
+import (
+       "context"
+       "github.com/docker/docker/api/types"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+
+       "testing"
+)
+
+type MockDockerClient struct {
+       mock.Mock
+}
+
+func (m *MockDockerClient) ImageList(ctx context.Context, options 
types.ImageListOptions) ([]types.ImageSummary, error) {
+       args := m.Called(ctx, options)
+       return args.Get(0).([]types.ImageSummary), args.Error(1)
+}
+
+func TestCheckImageExists(t *testing.T) {
+
+       tests := []struct {
+               lookup   string
+               images   []string
+               expected bool
+       }{
+               {"docker.io/example/app-image:latest", 
[]string{"docker.io/example/app-image:latest"}, true},
+               {"docker.io/demo/service-image:1.0", 
[]string{"demo/service-image:1.0"}, true},
+
+               {"docker.io/testuser/sample-app", 
[]string{"docker.io/testuser/sample-app:latest"}, true},
+               {"docker.io/testuser/sample-app", 
[]string{"testuser/sample-app:latest"}, true},
+
+               {"testuser/sample-app:dev", 
[]string{"docker.io/testuser/sample-app:dev"}, true},
+               {"testuser/sample-app:dev", 
[]string{"testuser/sample-app:dev"}, true},
+
+               {"docker.io/example/app-image:latest", 
[]string{"app-image:latest"}, false},
+               {"docker.io/testuser/sample-app", 
[]string{"sample-app:latest"}, false},
+               {"testuser/sample-app:dev", []string{"sample-app:dev"}, false},
+       }
+
+       for _, test := range tests {
+               ctx := context.Background()
+               mockClient := new(MockDockerClient)
+
+               mockClient.On("ImageList", ctx, 
mock.Anything).Return([]types.ImageSummary{
+                       {
+                               RepoTags: test.images,
+                       },
+               }, nil)
+
+               exists, err := CheckImageExists(mockClient, ctx, test.lookup)
+               assert.NoError(t, err, "Error should be nil")
+               assert.True(t, exists == test.expected, "Expected %t, got %t", 
test.expected, exists)
+       }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to