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
33
34 type MockRegistryServer struct {
35 started bool
36 server *httptest.Server
37 }
38
39
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
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
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
237
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
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
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
446 manifestByVersionMap, ok := manifests[stack]
447 if !ok {
448 notFound(res, req, "")
449 return
450 }
451 manifest, ok := manifestByVersionMap[version]
452 if ok {
453
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
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
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
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
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