Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Read editor and samples images for imagepuller from dashboard … #1918

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions pkg/deploy/image-puller/defaultimages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//
// 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
}
102 changes: 102 additions & 0 deletions pkg/deploy/image-puller/defaultimages_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// 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))
}
Loading
Loading