...

Source file src/github.com/redhat-developer/odo/tests/helper/registry_server/mock_registry.go

Documentation: github.com/redhat-developer/odo/tests/helper/registry_server

     1  package registry_server
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"log"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"reflect"
    16  	"runtime"
    17  	"strings"
    18  
    19  	"github.com/gorilla/handlers"
    20  	"github.com/gorilla/mux"
    21  	. "github.com/onsi/ginkgo/v2"
    22  	. "github.com/onsi/gomega"
    23  	"github.com/opencontainers/go-digest"
    24  	"github.com/opencontainers/image-spec/specs-go"
    25  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    26  )
    27  
    28  const (
    29  	_singleStackVersionName = "___SINGLE_VERSION__"
    30  )
    31  
    32  // MockRegistryServer is an implementation of a Devfile Registry Server,
    33  // inspired by the own Devfile Registry tests at https://github.com/devfile/registry-support/blob/main/index/server/pkg/server/endpoint_test.go.
    34  type MockRegistryServer struct {
    35  	started bool
    36  	server  *httptest.Server
    37  }
    38  
    39  // DevfileStack is the main struct for devfile stack
    40  type DevfileStack struct {
    41  	Name     string                `json:"name"`
    42  	Versions []DevfileStackVersion `json:"versions,omitempty"`
    43  }
    44  
    45  type DevfileStackVersion struct {
    46  	Version         string   `json:"version,omitempty"`
    47  	IsDefault       bool     `json:"default"`
    48  	SchemaVersion   string   `json:"schemaVersion,omitempty"`
    49  	StarterProjects []string `json:"starterProjects"`
    50  }
    51  
    52  var manifests map[string]map[string]ocispec.Manifest
    53  
    54  func init() {
    55  	manifests = make(map[string]map[string]ocispec.Manifest)
    56  	stackRoot := filepath.Join(getRegistryBasePath(), "stacks")
    57  	_, err := os.Stat(stackRoot)
    58  	if err != nil && errors.Is(err, fs.ErrNotExist) {
    59  		log.Fatalf("file not found: %v - reason: %s. Did you run 'make generate-test-registry-build'?", stackRoot, err)
    60  	}
    61  
    62  	listFilesInDir := func(p string, excludeIf func(f os.FileInfo) bool) (res []string, err error) {
    63  		file, err := os.Open(p)
    64  		if err != nil {
    65  			return nil, err
    66  		}
    67  		defer file.Close()
    68  		files, err := file.Readdir(0)
    69  		if err != nil {
    70  			return nil, err
    71  		}
    72  		for _, f := range files {
    73  			if excludeIf != nil && excludeIf(f) {
    74  				continue
    75  			}
    76  			res = append(res, f.Name())
    77  		}
    78  		return res, nil
    79  	}
    80  
    81  	newManifest := func() ocispec.Manifest {
    82  		return ocispec.Manifest{
    83  			Versioned: specs.Versioned{SchemaVersion: 2},
    84  			Config: ocispec.Descriptor{
    85  				MediaType: "application/vnd.devfileio.devfile.config.v2+json",
    86  			},
    87  		}
    88  	}
    89  
    90  	buildLayerForFile := func(fpath string) (layer ocispec.Descriptor, err error) {
    91  		stat, err := os.Stat(fpath)
    92  		if err != nil {
    93  			return ocispec.Descriptor{}, err
    94  		}
    95  
    96  		dgest, err := digestFile(fpath)
    97  		if err != nil {
    98  			return ocispec.Descriptor{}, err
    99  		}
   100  		layer.Digest = digest.Digest(dgest)
   101  
   102  		f := filepath.Base(filepath.Clean(fpath))
   103  		if f == "devfile.yaml" {
   104  			layer.MediaType = "application/vnd.devfileio.devfile.layer.v1"
   105  		} else if strings.HasSuffix(f, ".tar") {
   106  			layer.MediaType = "application/x-tar"
   107  		}
   108  
   109  		layer.Size = stat.Size()
   110  		layer.Annotations = map[string]string{
   111  			"org.opencontainers.image.title": f,
   112  		}
   113  		return layer, nil
   114  	}
   115  
   116  	excludeIfDirFn := func(f os.FileInfo) bool {
   117  		return f.IsDir()
   118  	}
   119  	excludeIfNotDirFn := func(f os.FileInfo) bool {
   120  		return !f.IsDir()
   121  	}
   122  
   123  	dirsInStacksRoot, err := listFilesInDir(stackRoot, excludeIfNotDirFn)
   124  	if err != nil {
   125  		log.Fatalf(err.Error())
   126  	}
   127  	for _, f := range dirsInStacksRoot {
   128  		manifests[f] = make(map[string]ocispec.Manifest)
   129  		versionList, err := listFilesInDir(filepath.Join(stackRoot, f), excludeIfNotDirFn)
   130  		if err != nil {
   131  			log.Fatalf(err.Error())
   132  		}
   133  		if len(versionList) == 0 {
   134  			// Possible stack with single unnamed version
   135  			stackFiles, err := listFilesInDir(filepath.Join(stackRoot, f), excludeIfDirFn)
   136  			if err != nil {
   137  				log.Fatalf(err.Error())
   138  			}
   139  			manifest := newManifest()
   140  			for _, vf := range stackFiles {
   141  				layer, err := buildLayerForFile(filepath.Join(stackRoot, f, vf))
   142  				if err != nil {
   143  					log.Fatalf(err.Error())
   144  				}
   145  				manifest.Layers = append(manifest.Layers, layer)
   146  			}
   147  			manifests[f][_singleStackVersionName] = manifest
   148  			continue
   149  		}
   150  		for _, v := range versionList {
   151  			versionFiles, err := listFilesInDir(filepath.Join(stackRoot, f, v), excludeIfDirFn)
   152  			if err != nil {
   153  				log.Fatalf(err.Error())
   154  			}
   155  			manifest := newManifest()
   156  			for _, vf := range versionFiles {
   157  				layer, err := buildLayerForFile(filepath.Join(stackRoot, f, v, vf))
   158  				if err != nil {
   159  					log.Fatalf(err.Error())
   160  				}
   161  				manifest.Layers = append(manifest.Layers, layer)
   162  			}
   163  			manifests[f][v] = manifest
   164  		}
   165  	}
   166  }
   167  
   168  func NewMockRegistryServer() *MockRegistryServer {
   169  	r := mux.NewRouter()
   170  	m := MockRegistryServer{
   171  		server: httptest.NewUnstartedServer(handlers.LoggingHandler(GinkgoWriter, r)),
   172  	}
   173  
   174  	m.setupRoutes(r)
   175  	return &m
   176  }
   177  
   178  func (m *MockRegistryServer) Start() (url string, err error) {
   179  	m.server.Start()
   180  	m.started = true
   181  	fmt.Fprintln(GinkgoWriter, "Mock Devfile Registry server started and available at", m.server.URL)
   182  	return m.server.URL, nil
   183  }
   184  
   185  func (m *MockRegistryServer) Stop() error {
   186  	m.server.Close()
   187  	m.started = false
   188  	return nil
   189  }
   190  
   191  func (m *MockRegistryServer) GetUrl() string {
   192  	return m.server.URL
   193  }
   194  
   195  func (m *MockRegistryServer) IsStarted() bool {
   196  	return m.started
   197  }
   198  
   199  func notFoundManifest(res http.ResponseWriter, req *http.Request, tag string) {
   200  	var data string
   201  	if req.Method == http.MethodGet {
   202  		data = fmt.Sprintf(`
   203  {
   204  	"code": "MANIFEST_UNKNOWN",
   205  	"message": "manifest unknown",
   206  	"detail": {
   207  		"tag": %s
   208  	}
   209  }
   210  `, tag)
   211  	}
   212  	res.WriteHeader(http.StatusNotFound)
   213  	_, err := res.Write([]byte(data))
   214  	if err != nil {
   215  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   216  	}
   217  }
   218  
   219  // notFound custom handler for anything not found
   220  func notFound(res http.ResponseWriter, req *http.Request, data string) {
   221  	res.WriteHeader(http.StatusNotFound)
   222  	_, err := res.Write([]byte(data))
   223  	if err != nil {
   224  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   225  	}
   226  }
   227  
   228  func internalServerError(res http.ResponseWriter, req *http.Request, data string) {
   229  	res.WriteHeader(http.StatusInternalServerError)
   230  	_, err := res.Write([]byte(fmt.Sprintf(`{"detail": %q}`, data)))
   231  	if err != nil {
   232  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   233  	}
   234  }
   235  
   236  // setupRoutes setups the routing, based on the OpenAPI Schema defined at:
   237  // https://github.com/devfile/registry-support/blob/main/index/server/openapi.yaml
   238  func (m *MockRegistryServer) setupRoutes(r *mux.Router) {
   239  	r.HandleFunc("/v2index", serveV2Index).Methods(http.MethodGet)
   240  	r.HandleFunc("/v2/devfile-catalog/{stack}/manifests/{ref}", serveManifests).Methods(http.MethodGet, http.MethodHead)
   241  	r.HandleFunc("/v2/devfile-catalog/{stack}/blobs/{digest}", serveBlobs).Methods(http.MethodGet)
   242  	r.HandleFunc("/devfiles/{stack}", m.serveDevfileDefaultVersion).Methods(http.MethodGet)
   243  	r.HandleFunc("/devfiles/{stack}/{version}", m.serveDevfileAtVersion).Methods(http.MethodGet)
   244  }
   245  
   246  func getRegistryBasePath() string {
   247  	_, filename, _, _ := runtime.Caller(1)
   248  	return filepath.Join(path.Dir(filename), "testdata", "registry-build")
   249  }
   250  
   251  func serveV2Index(res http.ResponseWriter, req *http.Request) {
   252  	index := filepath.Join(getRegistryBasePath(), "index.json")
   253  	d, err := os.ReadFile(index)
   254  	if err != nil {
   255  		internalServerError(res, req, err.Error())
   256  		return
   257  	}
   258  	_, err = res.Write(d)
   259  	if err != nil {
   260  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   261  	}
   262  }
   263  
   264  func serveManifests(res http.ResponseWriter, req *http.Request) {
   265  	vars := mux.Vars(req)
   266  	stack := vars["stack"]
   267  	ref := vars["ref"]
   268  	var (
   269  		stackManifest ocispec.Manifest
   270  		found         bool
   271  		bytes         []byte
   272  		err           error
   273  	)
   274  
   275  	if strings.HasPrefix(ref, "sha256:") {
   276  		var stackManifests map[string]ocispec.Manifest
   277  		stackManifests, found = manifests[stack]
   278  		if !found {
   279  			notFoundManifest(res, req, ref)
   280  			return
   281  		}
   282  		found = false
   283  		var dgst string
   284  		for _, manifest := range stackManifests {
   285  			dgst, err = digestEntity(manifest)
   286  			if err != nil {
   287  				internalServerError(res, req, "")
   288  				return
   289  			}
   290  			if reflect.DeepEqual(ref, dgst) {
   291  				stackManifest = manifest
   292  				found = true
   293  				break
   294  			}
   295  		}
   296  		if !found {
   297  			notFoundManifest(res, req, ref)
   298  			return
   299  		}
   300  	} else {
   301  		stackManifest, found = manifests[stack][ref]
   302  		if !found {
   303  			// Possible single unnamed version
   304  			stackManifest, found = manifests[stack][_singleStackVersionName]
   305  			if !found {
   306  				notFoundManifest(res, req, ref)
   307  				return
   308  			}
   309  		}
   310  	}
   311  
   312  	var j []byte
   313  	if j, err = json.MarshalIndent(stackManifest, " ", " "); err != nil {
   314  		fmt.Fprintln(GinkgoWriter, "[debug] stackManifest:", stackManifest)
   315  	} else {
   316  		fmt.Fprintln(GinkgoWriter, "[debug] stackManifest:", string(j))
   317  	}
   318  
   319  	if req.Method == http.MethodGet {
   320  		bytes, err = json.Marshal(stackManifest)
   321  		if err != nil {
   322  			internalServerError(res, req, err.Error())
   323  			return
   324  		}
   325  	}
   326  
   327  	res.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
   328  	res.WriteHeader(http.StatusOK)
   329  	_, err = res.Write(bytes)
   330  	if err != nil {
   331  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   332  	}
   333  }
   334  
   335  func serveBlobs(res http.ResponseWriter, req *http.Request) {
   336  	vars := mux.Vars(req)
   337  	stack := vars["stack"]
   338  	sDigest := vars["digest"]
   339  	stackRoot := filepath.Join(getRegistryBasePath(), "stacks", stack)
   340  	var (
   341  		blobPath string
   342  		found    bool
   343  		err      error
   344  	)
   345  
   346  	found = false
   347  	err = filepath.WalkDir(stackRoot, func(path string, d fs.DirEntry, err error) error {
   348  		var fdgst string
   349  
   350  		if err != nil {
   351  			return err
   352  		}
   353  
   354  		if found || d.IsDir() {
   355  			return nil
   356  		}
   357  
   358  		fdgst, err = digestFile(path)
   359  		if err != nil {
   360  			return err
   361  		}
   362  		if reflect.DeepEqual(sDigest, fdgst) {
   363  			blobPath = path
   364  			found = true
   365  		}
   366  
   367  		return nil
   368  	})
   369  	if err != nil || !found {
   370  		notFound(res, req, "")
   371  		return
   372  	}
   373  
   374  	file, err := os.Open(blobPath)
   375  	Expect(err).ShouldNot(HaveOccurred())
   376  	defer file.Close()
   377  
   378  	bytes, err := io.ReadAll(file)
   379  	Expect(err).ShouldNot(HaveOccurred())
   380  
   381  	res.WriteHeader(http.StatusOK)
   382  	res.Header().Set("Content-Type", http.DetectContentType(bytes))
   383  	_, err = res.Write(bytes)
   384  	if err != nil {
   385  		fmt.Fprintln(GinkgoWriter, "[warn] failed to write response; cause:", err)
   386  	}
   387  }
   388  
   389  func (m *MockRegistryServer) serveDevfileDefaultVersion(res http.ResponseWriter, req *http.Request) {
   390  	vars := mux.Vars(req)
   391  	stack := vars["stack"]
   392  
   393  	defaultVersion, internalErr, err := findStackDefaultVersion(stack)
   394  	if err != nil {
   395  		if internalErr {
   396  			internalServerError(res, req, "")
   397  		} else {
   398  			notFound(res, req, "")
   399  		}
   400  		return
   401  	}
   402  
   403  	http.Redirect(res, req, fmt.Sprintf("%s/devfiles/%s/%s", m.GetUrl(), stack, defaultVersion), http.StatusSeeOther)
   404  }
   405  
   406  func findStackDefaultVersion(stack string) (string, bool, error) {
   407  	index, err := parseIndex()
   408  	if index == nil {
   409  		return "", true, err
   410  	}
   411  	for _, d := range index {
   412  		if d.Name != stack {
   413  			continue
   414  		}
   415  		for _, v := range d.Versions {
   416  			if v.IsDefault {
   417  				return v.Version, false, nil
   418  			}
   419  		}
   420  	}
   421  	return "", false, fmt.Errorf("default version not found for %q", stack)
   422  }
   423  
   424  func parseIndex() ([]DevfileStack, error) {
   425  	// find the default version
   426  	index := filepath.Join(getRegistryBasePath(), "index.json")
   427  	d, err := os.ReadFile(index)
   428  	if err != nil {
   429  		return nil, err
   430  	}
   431  
   432  	var objmap []DevfileStack
   433  	err = json.Unmarshal(d, &objmap)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  	return objmap, nil
   438  }
   439  
   440  func (m *MockRegistryServer) serveDevfileAtVersion(res http.ResponseWriter, req *http.Request) {
   441  	vars := mux.Vars(req)
   442  	stack := vars["stack"]
   443  	version := vars["version"]
   444  
   445  	// find layer for this version and redirect to the blob download URL
   446  	manifestByVersionMap, ok := manifests[stack]
   447  	if !ok {
   448  		notFound(res, req, "")
   449  		return
   450  	}
   451  	manifest, ok := manifestByVersionMap[version]
   452  	if ok {
   453  		// find blob with devfile
   454  		for _, layer := range manifest.Layers {
   455  			if layer.Annotations["org.opencontainers.image.title"] == "devfile.yaml" {
   456  				http.Redirect(res, req, fmt.Sprintf("%s/v2/devfile-catalog/%s/blobs/%s", m.GetUrl(), stack, layer.Digest), http.StatusSeeOther)
   457  				return
   458  			}
   459  		}
   460  		notFound(res, req, "devfile.yaml not found")
   461  		return
   462  	}
   463  
   464  	// find if devfile has a single version that matches the default version in index
   465  	defaultVersion, internalErr, err := findStackDefaultVersion(stack)
   466  	if err != nil {
   467  		if internalErr {
   468  			internalServerError(res, req, "")
   469  		} else {
   470  			notFound(res, req, "")
   471  		}
   472  		return
   473  	}
   474  	if defaultVersion != version {
   475  		notFound(res, req, "default version for this stack is:"+defaultVersion)
   476  		return
   477  	}
   478  	manifest, ok = manifestByVersionMap[defaultVersion]
   479  	if ok {
   480  		// find blob with devfile
   481  		for _, layer := range manifest.Layers {
   482  			if layer.Annotations["org.opencontainers.image.title"] == "devfile.yaml" {
   483  				http.Redirect(res, req, fmt.Sprintf("%s/v2/devfile-catalog/%s/blobs/%s", m.GetUrl(), stack, layer.Digest), http.StatusSeeOther)
   484  				return
   485  			}
   486  		}
   487  		notFound(res, req, "devfile.yaml not found")
   488  		return
   489  	}
   490  }
   491  
   492  // digestEntity generates sha256 digest of any entity type
   493  func digestEntity(e interface{}) (string, error) {
   494  	bytes, err := json.Marshal(e)
   495  	if err != nil {
   496  		return "", err
   497  	}
   498  
   499  	return digest.FromBytes(bytes).String(), nil
   500  }
   501  
   502  // digestFile generates sha256 digest from file contents
   503  func digestFile(filepath string) (string, error) {
   504  	file, err := os.Open(filepath)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	defer file.Close()
   509  
   510  	dgst, err := digest.FromReader(file)
   511  	if err != nil {
   512  		return "", err
   513  	}
   514  
   515  	return dgst.String(), nil
   516  }
   517  

View as plain text