diff --git a/.github/install-guide.md b/.github/install-guide.md index 5233ff47..cf2186e6 100755 --- a/.github/install-guide.md +++ b/.github/install-guide.md @@ -18,7 +18,7 @@ The Docker setup is the default setup, and is ran with docker compose. This is [ **PS: if you're setting up Shuffle on Windows, go to the next step (Windows Docker setup)** -1. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed, and that you have a minimum of **2Gb of RAM** available. +1. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed, and that you have a minimum of **4Gb of RAM** available. 2. Download Shuffle ```bash git clone https://github.com/Shuffle/Shuffle diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 60e483cd..272235ed 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -4,7 +4,7 @@ go 1.24.0 toolchain go1.24.3 -replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared //replace github.com/frikky/schemaless => ../../../schemaless //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi @@ -24,7 +24,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.9.4 + github.com/shuffle/shuffle-shared v0.9.5 github.com/shuffle/singul v0.0.16 golang.org/x/crypto v0.40.0 google.golang.org/api v0.236.0 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index f5c3834e..31246221 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -363,8 +363,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shuffle/shuffle-shared v0.9.4 h1:xg8qjPgc1tCdIoM218LtQywNSs791m+Sn8F30pFb0W4= -github.com/shuffle/shuffle-shared v0.9.4/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= +github.com/shuffle/shuffle-shared v0.9.5 h1:4NBfNcw1VJMeU2gKH9B3pPEVbwEXwlandDtDJ60Rwn8= +github.com/shuffle/shuffle-shared v0.9.5/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E= github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/frontend/src/views/LoginPageOld.jsx b/frontend/src/views/LoginPageOld.jsx index aaee73d6..c0996436 100755 --- a/frontend/src/views/LoginPageOld.jsx +++ b/frontend/src/views/LoginPageOld.jsx @@ -344,7 +344,7 @@ const LoginDialog = (props) => { variant="body2" style={{ marginBottom: 20, color: "white" }} > - 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 2Gb of RAM available:{" "} + 1. Make sure shuffle-database folder has correct access, and that you have a minimum of 4Gb of RAM available:{" "}

sudo chown -R 1000:1000 shuffle-database diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index ce6e698d..81a661d3 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -10,7 +10,7 @@ require ( github.com/docker/docker v28.2.2+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.9.4 + github.com/shuffle/shuffle-shared v0.9.5 k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 ) diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 252f0afb..ca3f15d3 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -328,8 +328,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shuffle/shuffle-shared v0.9.4 h1:xg8qjPgc1tCdIoM218LtQywNSs791m+Sn8F30pFb0W4= -github.com/shuffle/shuffle-shared v0.9.4/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= +github.com/shuffle/shuffle-shared v0.9.5 h1:4NBfNcw1VJMeU2gKH9B3pPEVbwEXwlandDtDJ60Rwn8= +github.com/shuffle/shuffle-shared v0.9.5/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/functions/onprem/worker/go.mod b/functions/onprem/worker/go.mod index 48b8cd12..496b095e 100644 --- a/functions/onprem/worker/go.mod +++ b/functions/onprem/worker/go.mod @@ -11,7 +11,7 @@ require ( github.com/docker/docker v28.2.2+incompatible github.com/gorilla/mux v1.8.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.9.4 + github.com/shuffle/shuffle-shared v0.9.5 github.com/shuffle/singul v0.0.16 k8s.io/api v0.33.1 k8s.io/apimachinery v0.33.1 diff --git a/functions/onprem/worker/go.sum b/functions/onprem/worker/go.sum index f118b94f..49837b42 100644 --- a/functions/onprem/worker/go.sum +++ b/functions/onprem/worker/go.sum @@ -330,8 +330,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shuffle/shuffle-shared v0.9.4 h1:xg8qjPgc1tCdIoM218LtQywNSs791m+Sn8F30pFb0W4= -github.com/shuffle/shuffle-shared v0.9.4/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= +github.com/shuffle/shuffle-shared v0.9.5 h1:4NBfNcw1VJMeU2gKH9B3pPEVbwEXwlandDtDJ60Rwn8= +github.com/shuffle/shuffle-shared v0.9.5/go.mod h1:klSHfahYORgrQS4AdG5RIfTaL4lakbJ+R9EXfc7QQo0= github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E= github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index b6693d08..0db20491 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -24,7 +24,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" + dockerimage "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" dockerclient "github.com/docker/docker/client" @@ -868,7 +868,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] var err error if isKubernetes != "true" { - exposedPort, err = findAppInfo(image, appName) + exposedPort, err = findAppInfo(image, appName, false) if err != nil { log.Printf("[ERROR] Failed finding and creating port for %s: %s", appName, err) return err @@ -908,7 +908,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] waitTime := time.Duration(action.ExecutionDelay) * time.Second time.AfterFunc(waitTime, func() { - err = sendAppRequest(ctx, baseUrl, appName, exposedPort, &action, &workflowExecution) + err = sendAppRequest(ctx, baseUrl, appName, exposedPort, &action, &workflowExecution, image, 0) if err != nil { log.Printf("[ERROR] Failed sending SCHEDULED request to app %s on port %d: %s", appName, exposedPort, err) } @@ -923,7 +923,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env [] ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Cancel the context to release resources even if not used - go sendAppRequest(ctx, baseUrl, appName, exposedPort, &action, &workflowExecution) + go sendAppRequest(ctx, baseUrl, appName, exposedPort, &action, &workflowExecution, image, 0) }) } @@ -1635,7 +1635,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) { // This is the weirdest shit ever looking back at // Needs optimization lol - pullOptions := image.PullOptions{} + pullOptions := dockerimage.PullOptions{} if strings.ToLower(cleanupEnv) == "true" { err = deployApp(dockercli, images[0], identifier, env, workflowExecution, action) if err != nil && !strings.Contains(err.Error(), "Conflict. The container name") { @@ -2025,7 +2025,7 @@ func executionInit(workflowExecution shuffle.WorkflowExecution) error { return errors.New(fmt.Sprintf("No apps to handle onprem (%s)", environment)) } - pullOptions := image.PullOptions{} + pullOptions := dockerimage.PullOptions{} _ = pullOptions for _, image := range onpremApps { //log.Printf("[INFO] Image: %s", image) @@ -3183,6 +3183,31 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl log.Printf("[DEBUG] Deploying service for %s to swarm on port %d", name, deployport) //containerName := fmt.Sprintf("shuffle-worker-%s", parsedUuid) + + + // Check if the image exists or not - just in case + _, _, err := dockercli.ImageInspectWithRaw(context.Background(), image) + if err != nil { + log.Printf("[INFO] Image %s not found locally. Pulling from registry...", image) + + localRegistry := os.Getenv("REGISTRY_URL") + if !strings.HasPrefix(image, localRegistry) && len(localRegistry) > 0 { + image = fmt.Sprintf("%s/%s", localRegistry, image) + log.Printf("[DEBUG] Changed image to %s", image) + } + + _, err := dockercli.ImagePull( + context.Background(), + image, + dockerimage.PullOptions{}, + ) + if err != nil { + log.Printf("[ERROR] Failed pulling image %s: %s", image, err) + return err + } + } + + if len(baseimagename) == 0 || baseimagename == "/" { baseimagename = "frikky/shuffle" //var baseimagename = "frikky/shuffle" @@ -3195,7 +3220,7 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl networkName = swarmNetworkName } - replicas := uint64(1) + replicas := uint64(2) // Sent from Orborus // Should be equal to @@ -3349,7 +3374,7 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl //return err } - // Retry deploying the service + // Retry deploying the service (once) if !retry { return deploySwarmService(dockercli, name, image, deployport, true) } @@ -3366,6 +3391,163 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl /*** ENDREMOVE ***/ +func findAppInfo(image, name string, redeploy bool) (int, error) { + + highest := baseport + exposedPort := -1 + + // Exists as a "cache" layer + if portMappings != nil { + for key, value := range portMappings { + if value > highest { + highest = value + } + + if key == name { + exposedPort = value + break + } + } + } else { + portMappings = make(map[string]int) + } + + //Filters: + if exposedPort == -1 || redeploy { + dockercli, err := dockerclient.NewEnvClient() + if err != nil { + log.Printf("[ERROR] Unable to create docker client (2): %s", err) + return -1, err + } + + serviceListOptions := types.ServiceListOptions{} + services, err := dockercli.ServiceList( + context.Background(), + serviceListOptions, + ) + + // Basic self-correction + if err != nil { + log.Printf("[ERROR] Unable to list services: %s (may continue anyway?)", err) + if strings.Contains(fmt.Sprintf("%s", err), "is too new") { + // Static for some reason + defaultVersion := "1.40" + dockerApiVersion = defaultVersion + os.Setenv("DOCKER_API_VERSION", defaultVersion) + log.Printf("[DEBUG] Setting Docker API to %s default and retrying listing requests", defaultVersion) + } else { + return -1, err + } + + services, err = dockercli.ServiceList( + context.Background(), + serviceListOptions, + ) + + if err != nil { + log.Printf("[ERROR] Unable to list services (2): %s", err) + return -1, err + } + } + + for _, service := range services { + //log.Printf("[INFO] Service: %#v. Ports: %#v", service.Spec.Annotations.Name, service.Spec.EndpointSpec) + + for _, endpoint := range service.Spec.EndpointSpec.Ports { + if !strings.Contains(endpoint.Name, "port") { + continue + } + + portMappings[service.Spec.Annotations.Name] = int(endpoint.PublishedPort) + if int(endpoint.PublishedPort) > highest { + highest = int(endpoint.PublishedPort) + } + + if service.Spec.Annotations.Name == name || service.Spec.Annotations.Name == strings.Replace(name, ".", "-", -1) { + exposedPort = int(endpoint.PublishedPort) + //break + } + } + + if service.Spec.Annotations.Name != name && service.Spec.Annotations.Name != strings.Replace(name, ".", "-", -1) { + continue + } + + if redeploy { + // Remove the service and redeploy it. + // There are cases where the service doesn't update properly + // Check when the last update happened. If it was within the last 5 minutes, skip + if int(time.Since(service.UpdatedAt).Seconds()) > 600 { + + log.Printf("[INFO] Attempting redeploy of app %s with image %s since it is more than 10 minutes since last attempt with failure.", name, image) + + err = dockercli.ServiceRemove( + context.Background(), + service.ID, + ) + if err != nil { + log.Printf("[ERROR] Failed auto-removing service %s: %s", name, err) + } else { + log.Printf("[INFO] Auto-removed service %s successfully (rebuild due to redeploy).", name) + time.Sleep(10 * time.Second) + err = deploySwarmService( + dockercli, + name, + image, + exposedPort, + false, + ) + if err != nil { + log.Printf("[ERROR] Failed re-deploying service %s: %s", name, err) + } else { + time.Sleep(10 * time.Second) + } + } + } else { + //log.Printf("[INFO] NOT redeploying service %s since it was updated less than 10 minutes ago.", name) + } + } + + // Break if it's the correct port, as it's the right service + if exposedPort >= 0 { + break + } + } + } + + //log.Printf("[DEBUG] Portmappings: %#v", portMappings) + + if exposedPort >= 0 { + //log.Printf("[INFO] Found service %s on port %d - no need to deploy another", name, exposedPort) + } else { + dockercli, err := dockerclient.NewEnvClient() + if err != nil { + log.Printf("[ERROR] Unable to create docker client (2): %s", err) + return -1, err + } + + // Increment by 1 for highest port + if highest <= baseport { + highest = baseport + } + + highest += 1 + err = deploySwarmService(dockercli, name, image, highest, false) + if err != nil { + log.Printf("[WARNING] NOT Found service: %s. error: %s", name, err) + return highest, err + } else { + log.Printf("[DEBUG] Waiting 20 seconds before moving on to let app '%s' start properly. Service: %s (swarm)", name, image) + time.Sleep(time.Duration(20) * time.Second) + } + + exposedPort = highest + //return exposedPort, errors.New("Deployed app %s") + } + + return exposedPort, nil +} + // Runs data discovery /*** STARTREMOVE ***/ @@ -3511,129 +3693,11 @@ func initSwarmNetwork() error { return nil } -func findAppInfo(image, name string) (int, error) { - highest := baseport - exposedPort := -1 - // Exists as a "cache" layer - if portMappings != nil { - for key, value := range portMappings { - if value > highest { - highest = value - } - - if key == name { - exposedPort = value - break - } - } - } else { - portMappings = make(map[string]int) - } - - //Filters: - if exposedPort == -1 { - dockercli, err := dockerclient.NewEnvClient() - if err != nil { - log.Printf("[ERROR] Unable to create docker client (2): %s", err) - return -1, err - } - - serviceListOptions := types.ServiceListOptions{} - services, err := dockercli.ServiceList( - context.Background(), - serviceListOptions, - ) - - // Basic self-correction - if err != nil { - log.Printf("[ERROR] Unable to list services: %s (may continue anyway?)", err) - if strings.Contains(fmt.Sprintf("%s", err), "is too new") { - // Static for some reason - defaultVersion := "1.40" - dockerApiVersion = defaultVersion - os.Setenv("DOCKER_API_VERSION", defaultVersion) - log.Printf("[DEBUG] Setting Docker API to %s default and retrying listing requests", defaultVersion) - } else { - return -1, err - } - - services, err = dockercli.ServiceList( - context.Background(), - serviceListOptions, - ) - - if err != nil { - log.Printf("[ERROR] Unable to list services (2): %s", err) - return -1, err - } - } - - for _, service := range services { - //log.Printf("[INFO] Service: %#v", service.Spec.Annotations.Name) - - for _, endpoint := range service.Spec.EndpointSpec.Ports { - if strings.Contains(endpoint.Name, "port") { - portMappings[service.Spec.Annotations.Name] = int(endpoint.PublishedPort) - if int(endpoint.PublishedPort) > highest { - highest = int(endpoint.PublishedPort) - } - - if service.Spec.Annotations.Name == name || service.Spec.Annotations.Name == strings.Replace(name, ".", "-", -1) { - exposedPort = int(endpoint.PublishedPort) - //break - } - } - } - - //log.Printf("%s - %s", service.Spec.Annotations.Name, strings.Replace(name, ".", "-", -1)) - if service.Spec.Annotations.Name != name && service.Spec.Annotations.Name != strings.Replace(name, ".", "-", -1) { - continue - } - - // Break if it's the correct port, as it's the right service - if exposedPort >= 0 { - break - } - } - } - - //log.Printf("[DEBUG] Portmappings: %#v", portMappings) - - if exposedPort >= 0 { - //log.Printf("[INFO] Found service %s on port %d - no need to deploy another", name, exposedPort) - } else { - dockercli, err := dockerclient.NewEnvClient() - if err != nil { - log.Printf("[ERROR] Unable to create docker client (2): %s", err) - return -1, err - } - - // Increment by 1 for highest port - if highest <= baseport { - highest = baseport - } - - highest += 1 - err = deploySwarmService(dockercli, name, image, highest, false) - if err != nil { - log.Printf("[WARNING] NOT Found service: %s. error: %s", name, err) - return highest, err - } else { - log.Printf("[DEBUG] Waiting 20 seconds before moving on to let app '%s' start properly. Service: %s", name, image) - time.Sleep(time.Duration(20) * time.Second) - } - - exposedPort = highest - //return exposedPort, errors.New("Deployed app %s") - } - - return exposedPort, nil -} /*** ENDREMOVE ***/ -func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, action *shuffle.Action, workflowExecution *shuffle.WorkflowExecution) error { +func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, action *shuffle.Action, workflowExecution *shuffle.WorkflowExecution, image string, attempts int64) error { parsedRequest := shuffle.OrborusExecutionRequest{ Cleanup: cleanupEnv, ExecutionId: workflowExecution.ExecutionId, @@ -3780,12 +3844,28 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int, //var portMappings map[string]int } + // Try redeployment + attempts += 1 + if attempts < 2 { + // Check the service and fix it. + if isKubernetes == "true" { + log.Printf("[WARNING] App Redeployment in K8s isn't fully supported yet, but should be done for app %s with image %s.", appName, image) + } else { + _, err = findAppInfo(image, appName, true) + if err != nil { + log.Printf("[ERROR][%s] Error re-deploying app %s: %s", workflowExecution.ExecutionId, appName, err) + } + + return sendAppRequest(ctx, incomingUrl, appName, port, action, workflowExecution, image, attempts) + } + } + log.Printf("[ERROR][%s] Error running app run request: %s", workflowExecution.ExecutionId, err) actionResult := shuffle.ActionResult{ Action: *action, ExecutionId: workflowExecution.ExecutionId, Authorization: workflowExecution.Authorization, - Result: fmt.Sprintf(`{"success": false, "reason": "Failed to connect to app %s in swarm. Try the action again, restart Orborus if this is recurring, or contact support@shuffler.io.", "details": "%s"}`, streamUrl, newerr), + Result: fmt.Sprintf(`{"success": false, "attempts": %d, "reason": "Failed to connect to app %s in swarm. Try the action again, restart Orborus if this is recurring, or contact support@shuffler.io.", "details": "%s"}`, attempts, streamUrl, newerr), StartedAt: int64(time.Now().Unix()), CompletedAt: int64(time.Now().Unix()), Status: "FAILURE", @@ -3906,7 +3986,7 @@ func getStreamResultsWrapper(client *http.Client, req *http.Request, workflowExe } if newresp.StatusCode != 200 { - log.Printf("[ERROR] %sStatusCode (1): %d", string(body), newresp.StatusCode) + log.Printf("[ERROR] StatusCode (1): %d - %s", newresp.StatusCode, string(body)) time.Sleep(time.Duration(sleepTime) * time.Second) return environments, errors.New(fmt.Sprintf("Bad status code: %d", newresp.StatusCode)) } @@ -4558,7 +4638,7 @@ func handleDownloadImage(resp http.ResponseWriter, request *http.Request) { // check if images are already downloaded // Retrieve a list of Docker images - listOptions := image.ListOptions{} + listOptions := dockerimage.ListOptions{} images, err := client.ImageList(context.Background(), listOptions) if err != nil { log.Printf("[ERROR] listing images: %s", err)