diff --git a/Makefile b/Makefile index 514d711daa..a15fb17286 100644 --- a/Makefile +++ b/Makefile @@ -317,6 +317,7 @@ genenerate-env: | sed -E 's|(.*)=(.*)|\1="\2"|g' \ > $(BASH_ENV_FILE) echo "export WATCH_NAMESPACE=$${OPERATOR_NAMESPACE}" >> $(BASH_ENV_FILE) + echo "export CHE_OPERATOR__GET_EXTERNAL_IMAGES__IGNORE_FAILURES=true" >> $(BASH_ENV_FILE) echo "[INFO] Created $(BASH_ENV_FILE)" cat $(CONFIG_MANAGER) \ @@ -330,6 +331,7 @@ genenerate-env: | sed -E 's|(.*)=(.*)|\1="\2"|g' \ > $(VSCODE_ENV_FILE) echo "WATCH_NAMESPACE=$${OPERATOR_NAMESPACE}" >> $(VSCODE_ENV_FILE) + echo "CHE_OPERATOR__GET_EXTERNAL_IMAGES__IGNORE_FAILURES=true" >> $(VSCODE_ENV_FILE) echo "[INFO] Created $(VSCODE_ENV_FILE)" cat $(BASH_ENV_FILE) diff --git a/pkg/deploy/image-puller/defaultimages.go b/pkg/deploy/image-puller/defaultimages.go deleted file mode 100644 index cd2a540a2d..0000000000 --- a/pkg/deploy/image-puller/defaultimages.go +++ /dev/null @@ -1,262 +0,0 @@ -// -// Copyright (c) 2019-2023 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package imagepuller - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "sort" - "strings" - "time" - - "sigs.k8s.io/yaml" - - defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" -) - -// DefaultImagesProvider is an interface for fetching default images from a specific source. -type DefaultImagesProvider interface { - get(namespace string) ([]string, error) - persist(images []string, path string) error -} - -type DashboardApiDefaultImagesProvider struct { - DefaultImagesProvider - // introduce in order to override in tests - requestRawDataFunc func(url string) ([]byte, error) -} - -func NewDashboardApiDefaultImagesProvider() *DashboardApiDefaultImagesProvider { - return &DashboardApiDefaultImagesProvider{ - requestRawDataFunc: doRequestRawData, - } -} - -func (p *DashboardApiDefaultImagesProvider) get(namespace string) ([]string, error) { - editorsEndpointUrl := fmt.Sprintf( - "http://%s.%s.svc:8080/dashboard/api/editors", - defaults.GetCheFlavor()+"-dashboard", - namespace) - - editorsImages, err := p.readEditorImages(editorsEndpointUrl) - if err != nil { - return []string{}, fmt.Errorf("failed to read default images: %w from endpoint %s", err, editorsEndpointUrl) - } - - samplesEndpointUrl := fmt.Sprintf( - "http://%s.%s.svc:8080/dashboard/api/airgap-sample", - defaults.GetCheFlavor()+"-dashboard", - namespace) - - samplesImages, err := p.readSampleImages(samplesEndpointUrl) - if err != nil { - return []string{}, fmt.Errorf("failed to read default images: %w from endpoint %s", err, samplesEndpointUrl) - } - - // using map to avoid duplicates - allImages := make(map[string]bool) - - for _, image := range editorsImages { - allImages[image] = true - } - for _, image := range samplesImages { - allImages[image] = true - } - - // having them sorted, prevents from constant changing CR spec - return sortImages(allImages), nil -} - -// readEditorImages reads list of images from editors: -// 1. reads list of devfile editors from the given endpoint (json objects array) -// 2. parses them and return images -func (p *DashboardApiDefaultImagesProvider) readEditorImages(entrypointUrl string) ([]string, error) { - rawData, err := p.requestRawDataFunc(entrypointUrl) - if err != nil { - return []string{}, err - } - - return parseEditorDevfiles(rawData) -} - -// readSampleImages reads list of images from samples: -// 1. reads list of samples from the given endpoint (json objects array) -// 2. parses them and retrieves urls to a devfile -// 3. read and parses devfiles (yaml) and return images -func (p *DashboardApiDefaultImagesProvider) readSampleImages(entrypointUrl string) ([]string, error) { - rawData, err := p.requestRawDataFunc(entrypointUrl) - if err != nil { - return []string{}, err - } - - urls, err := parseSamples(rawData) - if err != nil { - return []string{}, err - } - - allImages := make([]string, 0) - for _, url := range urls { - rawData, err = p.requestRawDataFunc(url) - if err != nil { - return []string{}, err - } - - images, err := parseSampleDevfile(rawData) - if err != nil { - return []string{}, err - } - - allImages = append(allImages, images...) - } - - return allImages, nil -} - -func (p *DashboardApiDefaultImagesProvider) persist(images []string, path string) error { - return os.WriteFile(path, []byte(strings.Join(images, "\n")), 0644) -} - -func sortImages(images map[string]bool) []string { - sortedImages := make([]string, len(images)) - - i := 0 - for image := range images { - sortedImages[i] = image - i++ - } - - sort.Strings(sortedImages) - return sortedImages -} - -func doRequestRawData(url string) ([]byte, error) { - client := &http.Client{ - Transport: &http.Transport{}, - Timeout: time.Second * 1, - } - - request, err := http.NewRequest("GET", url, nil) - if err != nil { - return []byte{}, err - } - - response, err := client.Do(request) - if err != nil { - return []byte{}, err - } - - rawData, err := io.ReadAll(response.Body) - if err != nil { - return []byte{}, err - } - - _ = response.Body.Close() - return rawData, nil -} - -// parseSamples parse samples to collect urls to devfiles -func parseSamples(rawData []byte) ([]string, error) { - if len(rawData) == 0 { - return []string{}, nil - } - - var samples []interface{} - if err := json.Unmarshal(rawData, &samples); err != nil { - return []string{}, err - } - - urls := make([]string, 0) - - for i := range samples { - sample, ok := samples[i].(map[string]interface{}) - if !ok { - continue - } - - if sample["url"] != nil { - urls = append(urls, sample["url"].(string)) - } - } - - return urls, nil -} - -// parseDevfiles parse sample devfile represented as yaml to collect images -func parseSampleDevfile(rawData []byte) ([]string, error) { - if len(rawData) == 0 { - return []string{}, nil - } - - var devfile map[string]interface{} - if err := yaml.Unmarshal(rawData, &devfile); err != nil { - return []string{}, err - } - - return collectDevfileImages(devfile), nil -} - -// parseEditorDevfiles parse editor devfiles represented as json array to collect images -func parseEditorDevfiles(rawData []byte) ([]string, error) { - if len(rawData) == 0 { - return []string{}, nil - } - - var devfiles []interface{} - if err := json.Unmarshal(rawData, &devfiles); err != nil { - return []string{}, err - } - - images := make([]string, 0) - - for i := range devfiles { - devfile, ok := devfiles[i].(map[string]interface{}) - if !ok { - continue - } - - images = append(images, collectDevfileImages(devfile)...) - } - - return images, nil -} - -// collectDevfileImages retrieves images container component of the devfile. -func collectDevfileImages(devfile map[string]interface{}) []string { - devfileImages := make([]string, 0) - - components, ok := devfile["components"].([]interface{}) - if !ok { - return []string{} - } - - for k := range components { - component, ok := components[k].(map[string]interface{}) - if !ok { - continue - } - - container, ok := component["container"].(map[string]interface{}) - if !ok { - continue - } - - if container["image"] != nil { - devfileImages = append(devfileImages, container["image"].(string)) - } - } - - return devfileImages -} diff --git a/pkg/deploy/image-puller/defaultimages_test.go b/pkg/deploy/image-puller/defaultimages_test.go deleted file mode 100644 index c2287cfe04..0000000000 --- a/pkg/deploy/image-puller/defaultimages_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright (c) 2019-2024 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package imagepuller - -import ( - "fmt" - "os" - - defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" - - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestReadEditorImages(t *testing.T) { - imagesProvider := &DashboardApiDefaultImagesProvider{ - requestRawDataFunc: func(url string) ([]byte, error) { - return os.ReadFile("image-puller-resources-test/editors.json") - }, - } - - images, err := imagesProvider.readEditorImages("") - assert.NoError(t, err) - assert.Equal(t, 2, len(images)) - assert.Contains(t, images, "image_1") - assert.Contains(t, images, "image_2") -} - -func TestSampleImages(t *testing.T) { - imagesProvider := &DashboardApiDefaultImagesProvider{ - requestRawDataFunc: func(url string) ([]byte, error) { - switch url { - case "": - return os.ReadFile("image-puller-resources-test/samples.json") - case "sample_1_url": - return os.ReadFile("image-puller-resources-test/sample_1.yaml") - case "sample_2_url": - return os.ReadFile("image-puller-resources-test/sample_2.yaml") - default: - return []byte{}, fmt.Errorf("unexpected url: %s", url) - } - }, - } - - images, err := imagesProvider.readSampleImages("") - assert.NoError(t, err) - assert.Equal(t, 2, len(images)) - assert.Contains(t, images, "image_1") - assert.Contains(t, images, "image_3") -} - -func TestGet(t *testing.T) { - imagesProvider := &DashboardApiDefaultImagesProvider{ - requestRawDataFunc: func(url string) ([]byte, error) { - samplesEndpointUrl := fmt.Sprintf( - "http://%s.eclipse-che.svc:8080/dashboard/api/airgap-sample", - defaults.GetCheFlavor()+"-dashboard") - editorsEndpointUrl := fmt.Sprintf( - "http://%s.eclipse-che.svc:8080/dashboard/api/editors", - defaults.GetCheFlavor()+"-dashboard") - - switch url { - case editorsEndpointUrl: - return os.ReadFile("image-puller-resources-test/editors.json") - case samplesEndpointUrl: - return os.ReadFile("image-puller-resources-test/samples.json") - case "sample_1_url": - return os.ReadFile("image-puller-resources-test/sample_1.yaml") - case "sample_2_url": - return os.ReadFile("image-puller-resources-test/sample_2.yaml") - default: - return []byte{}, fmt.Errorf("unexpected url: %s", url) - } - }, - } - - images, err := imagesProvider.get("eclipse-che") - assert.NoError(t, err) - assert.Equal(t, 3, len(images)) - assert.Equal(t, "image_1", images[0]) - assert.Equal(t, "image_2", images[1]) - assert.Equal(t, "image_3", images[2]) - - err = imagesProvider.persist(images, "/tmp/images.txt") - assert.NoError(t, err) - - data, err := os.ReadFile("/tmp/images.txt") - assert.NoError(t, err) - - assert.Equal(t, "image_1\nimage_2\nimage_3", string(data)) -} diff --git a/pkg/deploy/image-puller/externalImages.go b/pkg/deploy/image-puller/externalImages.go new file mode 100644 index 0000000000..ce0042fcaa --- /dev/null +++ b/pkg/deploy/image-puller/externalImages.go @@ -0,0 +1,280 @@ +// +// Copyright (c) 2019-2023 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package imagepuller + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "time" + + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "sigs.k8s.io/yaml" + + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" +) + +const externalImagesStoreFileName = "external_images.txt" + +type ExternalImagesProvider struct { + // Path to store retrieved external images + imagesFilePath string + + // exposed for testing purpose only, func to get content by url + fetchRawDataFunc func(url string) ([]byte, error) +} + +func NewExternalImagesProvider() *ExternalImagesProvider { + p := &ExternalImagesProvider{ + imagesFilePath: filepath.Join(os.TempDir(), externalImagesStoreFileName), + fetchRawDataFunc: fetchRawData, + } + return p +} + +func (p *ExternalImagesProvider) Get(ctx *chetypes.DeployContext) ([]string, error) { + images, err := p.read(ctx) + if err != nil { + return []string{}, err + } + + err = p.write(images) + if err != nil { + return []string{}, err + } + + return images, nil +} + +func (p *ExternalImagesProvider) read(ctx *chetypes.DeployContext) ([]string, error) { + editorsImages, err := p.fetchEditorImages(ctx) + if err != nil { + return []string{}, err + } + + samplesImages, err := p.fetchSampleImages(ctx) + if err != nil { + return []string{}, err + } + + var images []string + images = append(images, editorsImages...) + images = append(images, samplesImages...) + sort.Strings(images) + images = slices.Compact(images) + + return images, nil +} + +func (p *ExternalImagesProvider) write(images []string) error { + return os.WriteFile(p.imagesFilePath, []byte(strings.Join(images, "\n")), 0644) +} + +// fetchSampleImages fetches list of images from samples: +// 1. reads list of samples from the given endpoint (json objects array) +// 2. parses them and retrieves urls to a devfile +// 3. read and parses devfiles (yaml) and return images +func (p *ExternalImagesProvider) fetchSampleImages(ctx *chetypes.DeployContext) ([]string, error) { + url := getDashboardSamplesInternalAPIUrl(ctx) + + rawData, err := p.fetchRawDataFunc(url) + if err != nil { + return []string{}, err + } + + urls, err := p.parseSampleURLs(rawData) + if err != nil { + return []string{}, err + } + + sampleImages := make([]string, 0) + for _, url := range urls { + rawData, err = p.fetchRawDataFunc(url) + if err != nil { + return []string{}, err + } + + images, err := p.parseSampleDevfile(rawData) + if err != nil { + return []string{}, err + } + + sampleImages = append(sampleImages, images...) + } + + return sampleImages, nil +} + +// parseSampleURLs parses samples to collect urls to devfiles +func (p *ExternalImagesProvider) parseSampleURLs(rawData []byte) ([]string, error) { + if len(rawData) == 0 { + return []string{}, nil + } + + var samples []interface{} + if err := json.Unmarshal(rawData, &samples); err != nil { + return []string{}, err + } + + urls := make([]string, 0) + + for i := range samples { + sample, ok := samples[i].(map[string]interface{}) + if !ok { + continue + } + + url, ok := sample["url"].(string) + if ok { + urls = append(urls, url) + } + } + + return urls, nil +} + +// parseDevfiles parse sample devfile represented as yaml to collect images +func (p *ExternalImagesProvider) parseSampleDevfile(rawData []byte) ([]string, error) { + var devfile map[string]interface{} + if err := yaml.Unmarshal(rawData, &devfile); err != nil { + return []string{}, err + } + + return p.extractContainerImages(devfile), nil +} + +// fetchEditorImages fetches list of images from editors: +// 1. reads list of devfile editors from the given endpoint (json objects array) +// 2. parses them and return images +func (p *ExternalImagesProvider) fetchEditorImages(ctx *chetypes.DeployContext) ([]string, error) { + url := getDashboardEditorsInternalAPIUrl(ctx) + + rawData, err := p.fetchRawDataFunc(url) + if err != nil { + return []string{}, err + } + + images, err := p.parseEditor(rawData) + if err != nil { + return []string{}, err + } + + return images, nil +} + +// parseEditor parse editor devfiles represented as json array to collect images +func (p *ExternalImagesProvider) parseEditor(rawData []byte) ([]string, error) { + if len(rawData) == 0 { + return []string{}, nil + } + + var devfiles []interface{} + if err := json.Unmarshal(rawData, &devfiles); err != nil { + return []string{}, err + } + + images := make([]string, 0) + + for i := range devfiles { + devfile, ok := devfiles[i].(map[string]interface{}) + if !ok { + continue + } + + images = append(images, p.extractContainerImages(devfile)...) + } + + return images, nil +} + +// extractContainerImages retrieves images from container components of the devfile. +func (p *ExternalImagesProvider) extractContainerImages(devfile map[string]interface{}) []string { + devfileImages := make([]string, 0) + + components, ok := devfile["components"].([]interface{}) + if !ok { + return []string{} + } + + for k := range components { + component, ok := components[k].(map[string]interface{}) + if !ok { + continue + } + + container, ok := component["container"].(map[string]interface{}) + if !ok { + continue + } + + if image, ok := container["image"].(string); ok { + devfileImages = append(devfileImages, image) + } + } + + return devfileImages +} + +func getDashboardBaseInternalURL(ctx *chetypes.DeployContext) string { + namespace := ctx.CheCluster.Namespace + serviceName := defaults.GetCheFlavor() + "-dashboard" + return fmt.Sprintf("http://%s.%s.svc:8080/dashboard/api", serviceName, namespace) +} + +func getDashboardEditorsInternalAPIUrl(ctx *chetypes.DeployContext) string { + return fmt.Sprintf("%s/editors", getDashboardBaseInternalURL(ctx)) +} + +func getDashboardSamplesInternalAPIUrl(ctx *chetypes.DeployContext) string { + return fmt.Sprintf("%s/airgap-sample", getDashboardBaseInternalURL(ctx)) +} + +func fetchRawData(url string) ([]byte, error) { + client := &http.Client{ + Transport: &http.Transport{}, + Timeout: time.Second * 5, + } + + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return []byte{}, err + } + + response, err := client.Do(request) + if err != nil { + return []byte{}, err + } + + defer func() { + if err := response.Body.Close(); err != nil { + logger.Error(err, "unable to close response body") + } + }() + + if response.StatusCode < 200 || response.StatusCode >= 300 { + return []byte{}, fmt.Errorf("unexpected status code %d for URL %s", response.StatusCode, url) + } + + rawData, err := io.ReadAll(response.Body) + if err != nil { + return []byte{}, err + } + + return rawData, nil +} diff --git a/pkg/deploy/image-puller/externalImages_test.go b/pkg/deploy/image-puller/externalImages_test.go new file mode 100644 index 0000000000..1c13dcf90e --- /dev/null +++ b/pkg/deploy/image-puller/externalImages_test.go @@ -0,0 +1,88 @@ +// +// Copyright (c) 2019-2024 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package imagepuller + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/eclipse-che/che-operator/pkg/common/test" + + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetExternalImages(t *testing.T) { + type testCase struct { + name string + editorsFile string + samplesFile string + expectedImages []string + } + + testCases := []testCase{ + { + name: "both editors and samples images", + editorsFile: "image-puller-resources-test/editors.json", + samplesFile: "image-puller-resources-test/samples.json", + expectedImages: []string{"image_1", "image_2", "image_3"}, + }, + { + name: "no external images", + editorsFile: "image-puller-resources-test/empty.json", + samplesFile: "image-puller-resources-test/empty.json", + expectedImages: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := test.NewCtxBuilder().Build() + + editorsEndpointUrl := getDashboardEditorsInternalAPIUrl(ctx) + samplesEndpointUrl := getDashboardSamplesInternalAPIUrl(ctx) + + imagesProvider := &ExternalImagesProvider{ + imagesFilePath: filepath.Join(os.TempDir(), externalImagesStoreFileName), + fetchRawDataFunc: func(url string) ([]byte, error) { + switch url { + case editorsEndpointUrl: + return os.ReadFile(tc.editorsFile) + case samplesEndpointUrl: + return os.ReadFile(tc.samplesFile) + case "sample_1_url": + return os.ReadFile("image-puller-resources-test/sample_1.yaml") + case "sample_2_url": + return os.ReadFile("image-puller-resources-test/sample_2.yaml") + default: + return []byte{}, fmt.Errorf("unexpected url: %s", url) + } + }, + } + + images, err := imagesProvider.Get(ctx) + + assert.NoError(t, err) + assert.Equal(t, tc.expectedImages, images) + + data, err := os.ReadFile(filepath.Join(os.TempDir(), externalImagesStoreFileName)) + assert.NoError(t, err) + + expectedFileContent := strings.Join(tc.expectedImages, "\n") + assert.Equal(t, expectedFileContent, string(data)) + }) + } +} diff --git a/pkg/deploy/image-puller/image-puller-resources-test/editors.json b/pkg/deploy/image-puller/image-puller-resources-test/editors.json index 612e150643..b49062a780 100644 --- a/pkg/deploy/image-puller/image-puller-resources-test/editors.json +++ b/pkg/deploy/image-puller/image-puller-resources-test/editors.json @@ -21,6 +21,15 @@ } ] }, + { + "components": [ + { + "container": { + "image": "image_2" + } + } + ] + }, { "components": [ { diff --git a/pkg/deploy/image-puller/image-puller-resources-test/empty.json b/pkg/deploy/image-puller/image-puller-resources-test/empty.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/pkg/deploy/image-puller/image-puller-resources-test/empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pkg/deploy/image-puller/image-puller-resources-test/sample_2.yaml b/pkg/deploy/image-puller/image-puller-resources-test/sample_2.yaml index cd3f24145b..062adf7f6d 100644 --- a/pkg/deploy/image-puller/image-puller-resources-test/sample_2.yaml +++ b/pkg/deploy/image-puller/image-puller-resources-test/sample_2.yaml @@ -18,4 +18,6 @@ components: container: image: image_3 - name: tools_2 - + - name: tools_3 + container: + image: image_1 diff --git a/pkg/deploy/image-puller/imagepuller.go b/pkg/deploy/image-puller/imagepuller.go index b728c7cc01..82949c35a7 100644 --- a/pkg/deploy/image-puller/imagepuller.go +++ b/pkg/deploy/image-puller/imagepuller.go @@ -15,6 +15,7 @@ package imagepuller import ( "errors" "fmt" + "os" "strings" "time" @@ -49,38 +50,31 @@ const ( defaultConfigMapName = "k8s-image-puller" defaultDeploymentName = "kubernetes-image-puller" defaultImagePullerImage = "quay.io/eclipse/kubernetes-image-puller:next" - - externalImagesFilePath = "/tmp/external_images.txt" ) type ImagePuller struct { reconciler.Reconcilable - imageProvider DefaultImagesProvider + externalImages *ExternalImagesProvider } func NewImagePuller() *ImagePuller { return &ImagePuller{ - imageProvider: NewDashboardApiDefaultImagesProvider(), + externalImages: NewExternalImagesProvider(), } } func (ip *ImagePuller) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { - defaultImages, err := ip.imageProvider.get(ctx.CheCluster.Namespace) + externalImages, err := ip.externalImages.Get(ctx) if err != nil { - logger.Error(err, "Failed to get default images", "error", err) - - // Don't block the reconciliation if we can't get the default images - return reconcile.Result{}, true, nil - } - - // Always fetch and persist the default images before actual sync. - // The purpose is to ability read them from the file on demand by admin (should be documented) - err = ip.imageProvider.persist(defaultImages, externalImagesFilePath) - if err != nil { - logger.Error(err, "Failed to save default images", "error", err) - - // Don't block the reconciliation if we can't save the default images on FS - return reconcile.Result{}, true, nil + // Previously, failures to get external images didn't block reconciliation. + // Actually it should not happen, because all requests go to dashboard service (not external url). + // Added a workaround env var to ignore failures in case of any possible issues. + ignoreGetExternalImagesFailures := os.Getenv("CHE_OPERATOR__GET_EXTERNAL_IMAGES__IGNORE_FAILURES") + if ignoreGetExternalImagesFailures == "true" { + logger.Error(err, "Failed to get external images") + } else { + return reconcile.Result{}, false, err + } } if ctx.CheCluster.Spec.Components.ImagePuller.Enable { @@ -89,7 +83,7 @@ func (ip *ImagePuller) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, return reconcile.Result{}, false, errors.New(errMsg) } - if done, err := ip.syncKubernetesImagePuller(defaultImages, ctx); !done { + if done, err := ip.syncKubernetesImagePuller(externalImages, ctx); !done { return reconcile.Result{RequeueAfter: time.Second}, false, err } } else { @@ -109,7 +103,7 @@ func (ip *ImagePuller) Finalize(ctx *chetypes.DeployContext) bool { } func (ip *ImagePuller) uninstallImagePuller(ctx *chetypes.DeployContext) (bool, error) { - // Keep it here for backward compatability + // Keep it here for backward compatibility if err := deploy.DeleteFinalizer(ctx, finalizerName); err != nil { return false, err } @@ -129,7 +123,7 @@ func (ip *ImagePuller) uninstallImagePuller(ctx *chetypes.DeployContext) (bool, return true, nil } -func (ip *ImagePuller) syncKubernetesImagePuller(defaultImages []string, ctx *chetypes.DeployContext) (bool, error) { +func (ip *ImagePuller) syncKubernetesImagePuller(externalImages []string, ctx *chetypes.DeployContext) (bool, error) { imagePuller := &chev1alpha1.KubernetesImagePuller{ TypeMeta: metav1.TypeMeta{ APIVersion: chev1alpha1.GroupVersion.String(), @@ -153,7 +147,7 @@ func (ip *ImagePuller) syncKubernetesImagePuller(defaultImages []string, ctx *ch imagePuller.Spec.DeploymentName = utils.GetValue(imagePuller.Spec.DeploymentName, defaultDeploymentName) imagePuller.Spec.ImagePullerImage = utils.GetValue(imagePuller.Spec.ImagePullerImage, defaultImagePullerImage) if strings.TrimSpace(imagePuller.Spec.Images) == "" { - imagePuller.Spec.Images = convertToSpecField(defaultImages) + imagePuller.Spec.Images = convertToSpecField(externalImages) } return deploy.SyncForClient(ctx.ClusterAPI.NonCachingClient, ctx, imagePuller, kubernetesImagePullerDiffOpts) @@ -173,7 +167,7 @@ func convertToSpecField(images []string) string { name = "image" } - // Adding index make the name unique + // Adding index makes the name unique specField += fmt.Sprintf("%s-%d=%s;", name, index, image) } diff --git a/pkg/deploy/image-puller/imagepuller_test.go b/pkg/deploy/image-puller/imagepuller_test.go index ce70e70cba..487859a6e2 100644 --- a/pkg/deploy/image-puller/imagepuller_test.go +++ b/pkg/deploy/image-puller/imagepuller_test.go @@ -15,6 +15,7 @@ package imagepuller import ( "context" "os" + "path/filepath" "sigs.k8s.io/controller-runtime/pkg/client" @@ -141,8 +142,9 @@ func TestImagePullerConfiguration(t *testing.T) { ctx := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.initObjects...).Build() ip := &ImagePuller{ - imageProvider: &DashboardApiDefaultImagesProvider{ - requestRawDataFunc: func(url string) ([]byte, error) { + externalImages: &ExternalImagesProvider{ + imagesFilePath: filepath.Join(os.TempDir(), externalImagesStoreFileName), + fetchRawDataFunc: func(url string) ([]byte, error) { return os.ReadFile(testCase.testCaseFilePath) }, },