diff --git a/controllers/general.go b/controllers/general.go index f3fa9ec..a5de154 100755 --- a/controllers/general.go +++ b/controllers/general.go @@ -43,7 +43,8 @@ func (o *GeneralController) GetAll() { Groups: groups, Admin: true, } - newWorkflow, err = newWorkflow.ExtractFromPlantUML(file, req) + var importWarnings []string + newWorkflow, importWarnings, err = newWorkflow.ExtractFromPlantUML(file, req) if err != nil { o.Data["json"] = map[string]interface{}{ "data": nil, @@ -55,9 +56,10 @@ func (o *GeneralController) GetAll() { } o.Data["json"] = map[string]interface{}{ - "data": newWorkflow, - "code": 200, - "error": nil, + "data": newWorkflow, + "code": 200, + "error": nil, + "warnings": importWarnings, } o.ServeJSON() @@ -93,7 +95,6 @@ func Websocket(ctx context.Context, user string, groups []string, dataType int, for { select { case msg, ok := <-infrastructure.SearchStream[user]: - fmt.Println("msg", msg, ok) if !ok { continue } diff --git a/controllers/resource.go b/controllers/resource.go index 0df782c..ff70d83 100755 --- a/controllers/resource.go +++ b/controllers/resource.go @@ -113,12 +113,12 @@ func (o *ResourceController) Search() { limit, _ := strconv.Atoi(o.Ctx.Input.Query("limit")) m := map[string][]utils.ShallowDBObject{} - fmt.Println(o.collection(false)) for _, col := range o.collection(false) { if m[col.String()] == nil { m[col.String()] = []utils.ShallowDBObject{} } s := oclib.NewRequest(col, user, peerID, groups, nil).Search(nil, search, isDraft == "true", int64(offset), int64(limit)) + fmt.Println(col, len(s.Data), s.Err) m[col.String()] = append(m[col.String()], s.Data...) } mm := map[string]interface{}{} @@ -154,6 +154,7 @@ func GetResource(typ oclib.LibDataEnum) interface{} { // @Title Search // @Description search workspace +// @Param type path string true "the type you want to get" // @Param is_draft query string false // @Param extend query string false "extend" // @Param offset query string false @@ -162,6 +163,7 @@ func GetResource(typ oclib.LibDataEnum) interface{} { // @Success 200 {workspace} models.workspace // @router /:type/extended/search [post] func (o *ResourceController) SearchExtended() { + fmt.Println("THERE") user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) // store and return Id or post with UUIDLibDataEnum isDraft := o.Ctx.Input.Query("is_draft") @@ -175,6 +177,7 @@ func (o *ResourceController) SearchExtended() { if m[col.String()] == nil { m[col.String()] = []utils.ShallowDBObject{} } + fmt.Println("filters", oclib.FiltersFromFlatMap(res, GetResource(col))) s := oclib.NewRequest(col, user, peerID, groups, nil).Search( oclib.FiltersFromFlatMap(res, GetResource(col)), "", isDraft == "true", int64(offset), int64(limit)) m[col.String()] = append(m[col.String()], s.Data...) @@ -203,10 +206,8 @@ func (o *ResourceController) Get() { user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) id := o.Ctx.Input.Param(":id") extend := strings.Split(o.Ctx.Input.Query("extend"), ",") - fmt.Println("COLLECTIOn", o.collection(false)) for _, col := range o.collection(false) { if d := oclib.NewRequest(col, user, peerID, groups, nil).LoadOne(id); d.Data != nil { - fmt.Println("EXTEND", d.Data.Extend(extend...), extend) o.Data["json"] = oclib.GetExtend(d.Data, d.Data.Extend(extend...), map[tools.DataType]map[string]interface{}{}) break } else { @@ -235,12 +236,23 @@ func (o *ResourceController) Post() { var res map[string]interface{} json.Unmarshal(o.Ctx.Input.CopyBody(100000), &res) + // Block "add from discovered cache": refuse resources whose creator_id belongs to another peer. + // Resources discovered via decentralized search must go to the workspace, not the local catalog. + if creatorID, _ := res["creator_id"].(string); creatorID != "" && peerID != "" && creatorID != peerID { + o.Data["json"] = map[string]interface{}{ + "data": nil, + "code": 403, + "err": "cannot add a resource created by another peer to the local catalog; use workspace instead", + } + o.ServeJSON() + return + } + // Remplace les sources privées (isReachable=false) par des clés opaques // avant la persistance : le vrai path ne doit jamais sortir de ce peer. replacePrivateSources(res) data := oclib.NewRequest(libs[0], user, peerID, groups, nil).StoreOne(res) - fmt.Println(data.Data, res["name"], libs[0]) if data.Err == "" { payload, _ := json.Marshal(data.Data.Serialize(data.Data)) infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{ @@ -338,12 +350,10 @@ func (o *ResourceController) Put() { // @Success 200 {resource} models.resource // @router /:type/:id [delete] func (o *ResourceController) Delete() { - fmt.Println("THERE") user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) id := o.Ctx.Input.Param(":id") for _, col := range o.collection(false) { data := oclib.NewRequest(col, user, peerID, groups, nil).DeleteOne(id) - fmt.Println(col, data, id) if data.Err == "" { o.Data["json"] = data payload, _ := json.Marshal(data.Data.Serialize(data.Data)) diff --git a/go.mod b/go.mod index 92962a4..ae4167a 100755 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module oc-catalog go 1.25.0 require ( - cloud.o-forge.io/core/oc-lib v0.0.0-20260529071252-7e5b69b1d2db + cloud.o-forge.io/core/oc-lib v0.0.0-20260622055001-58e97fbe7486 github.com/beego/beego/v2 v2.3.8 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/smartystreets/goconvey v1.7.2 diff --git a/go.sum b/go.sum index c2923ec..8f69e26 100755 --- a/go.sum +++ b/go.sum @@ -38,6 +38,34 @@ cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b h1:TWhmHeurbBmdy cloud.o-forge.io/core/oc-lib v0.0.0-20260527135023-cef23b5f307b/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= cloud.o-forge.io/core/oc-lib v0.0.0-20260529071252-7e5b69b1d2db h1:FxnOSk3PfgkSza6VVz9ZHga1kIQxioJDIVpy2T254j4= cloud.o-forge.io/core/oc-lib v0.0.0-20260529071252-7e5b69b1d2db/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529082207-ce110ee63434 h1:nw4i7WsibzxzYUf5jcpKDGS+fUmAf+8PK+8NrdbyuyI= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529082207-ce110ee63434/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529083845-b1429596bb9c h1:Rt+ta/Z35Vxfj3WKiQzpK2NbJ3UgXIm5jN9XNhCnexI= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529083845-b1429596bb9c/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529105104-41706949fd6b h1:XZUtsvUQfwHSVYoU2qrY+jPii+a5pDJfrS3RSvR5ZlU= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529105104-41706949fd6b/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529114341-a3bca2498217 h1:qSvncC081M+MktDyzfToeoaWITewxF4f5PVQQnYXaPk= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529114341-a3bca2498217/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529121240-82a4708f461a h1:uSCqskbJ2JoDGiuukMMDVXBH2yRh+XZZMhtAzCkibUk= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529121240-82a4708f461a/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529123144-afd8a2d97c13 h1:5yq9kJ6Hmpt2OkFXkIvHKtEe7yMI/l/DKExrXLZlG/8= +cloud.o-forge.io/core/oc-lib v0.0.0-20260529123144-afd8a2d97c13/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260601144505-71ae0d2cfc52 h1:AGFR3Dzl0dqD/98VhB54NXf6sxR13GB5R34b9WW2eWk= +cloud.o-forge.io/core/oc-lib v0.0.0-20260601144505-71ae0d2cfc52/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602064220-797df972ac5e h1:Q72L9lJa53VPMo7q/Osp9ffTIXWmD1uiy8F5Np0ZiF0= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602064220-797df972ac5e/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602085042-3924fca2896c h1:KMlI+uxMbWhjFIjlj+FGWPtSFMqSFroRP2K6eZmBf9Q= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602085042-3924fca2896c/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602093519-a7d0c1208b77 h1:Ts0+Yu5XoL4w5ywU4YUvCl/XDoFFPoqvZ+xVze2e+jA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260602093519-a7d0c1208b77/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260603092341-842364d145c5 h1:+Tc8po4TXm2uWEleUiEqxTo9CPYirYurhFDvcwGlZTg= +cloud.o-forge.io/core/oc-lib v0.0.0-20260603092341-842364d145c5/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260604070031-d19ff1f8b206 h1:HRbUR9H9CIblNC0Z8obgp2J3jW4d9nO0MLNw/aiZRRs= +cloud.o-forge.io/core/oc-lib v0.0.0-20260604070031-d19ff1f8b206/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260605135650-1425a3149455 h1:hDsqGw1EUY2b4mB+aUFSuQO75t+l+Ow9vZgjHZDK3uw= +cloud.o-forge.io/core/oc-lib v0.0.0-20260605135650-1425a3149455/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= +cloud.o-forge.io/core/oc-lib v0.0.0-20260622055001-58e97fbe7486 h1:ggVkk+44VulhKD13wu8g7b7sje0KzW+1QiHXxkNXTd8= +cloud.o-forge.io/core/oc-lib v0.0.0-20260622055001-58e97fbe7486/go.mod h1:JynnOb3eMr9VZW1mHq+Vsl3tzx6gPhPsGKpQD/dtEBc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= diff --git a/infrastructure/docker_scraper.go b/infrastructure/docker_scraper.go index 55f038f..0a1ac42 100644 --- a/infrastructure/docker_scraper.go +++ b/infrastructure/docker_scraper.go @@ -13,13 +13,22 @@ package infrastructure // // Environment variables (all optional): // -// DOCKER_SCRAPER_ENABLED true | false (default: true) -// DOCKER_SCRAPER_IMAGES comma-separated list of images to track. -// Format: "name" for official library images, -// "org/name" for user/org images. -// Default: a curated set of popular official images. -// DOCKER_SCRAPER_MAX_TAGS max tags to import per image (default: 10) -// DOCKER_SCRAPER_INTERVAL_H refresh interval in hours (default: 24) +// DOCKER_SCRAPER_ENABLED true | false (default: true) +// DOCKER_SCRAPER_IMAGES comma-separated list of images to track. +// Format: "name" for official library images, +// "org/name" for user/org images. +// Default: a curated set of popular official images. +// DOCKER_SCRAPER_MAX_TAGS max tags to import per image (default: 10) +// DOCKER_SCRAPER_INTERVAL_H refresh interval in hours (default: 24) +// +// Network endpoints (override to use a corporate proxy or private mirror): +// +// DOCKER_SCRAPER_HUB_URL Docker Hub API base URL (default: https://hub.docker.com) +// DOCKER_SCRAPER_REGISTRY_URL Docker registry base URL (default: https://registry-1.docker.io) +// DOCKER_SCRAPER_AUTH_URL Docker auth service URL (default: https://auth.docker.io) +// +// If none of the three endpoints are reachable at startup the scraper disables +// itself automatically rather than spinning with repeated timeouts. import ( "encoding/json" @@ -56,6 +65,9 @@ type scraperConfig struct { Images []DockerImageSpec MaxTags int IntervalHours int + HubURL string // base URL for the Docker Hub API + RegistryURL string // base URL for the Docker registry API + AuthURL string // base URL for the Docker auth service } // defaultImages is the baseline catalog seeded when DOCKER_SCRAPER_IMAGES is not set. @@ -241,11 +253,21 @@ var defaultImages = []DockerImageSpec{ // scraperConfigFromEnv reads scraper configuration from environment variables // and returns a populated scraperConfig with sensible defaults. func scraperConfigFromEnv() scraperConfig { + envOrDefault := func(key, def string) string { + if v := os.Getenv(key); v != "" { + return strings.TrimRight(v, "/") + } + return def + } + cfg := scraperConfig{ Enabled: true, MaxTags: 10, IntervalHours: 24, Images: defaultImages, + HubURL: envOrDefault("DOCKER_SCRAPER_HUB_URL", "https://hub.docker.com"), + RegistryURL: envOrDefault("DOCKER_SCRAPER_REGISTRY_URL", "https://registry-1.docker.io"), + AuthURL: envOrDefault("DOCKER_SCRAPER_AUTH_URL", "https://auth.docker.io"), } if v := os.Getenv("DOCKER_SCRAPER_ENABLED"); v == "false" { @@ -306,6 +328,150 @@ type hubTagsResponse struct { Results []hubTag `json:"results"` } +// registryClient is shared across all registry API calls with a sensible timeout. +var registryClient = &http.Client{Timeout: 15 * time.Second} + +// ─── Docker Registry API (manifest / config) ────────────────────────────────── + +type registryTokenResponse struct { + Token string `json:"token"` +} + +// registryManifestOrList decodes either a manifest list (multi-arch) or a plain +// image manifest. We check MediaType to tell them apart. +type registryManifestOrList struct { + MediaType string `json:"mediaType"` + SchemaVersion int `json:"schemaVersion"` + // manifest list fields + Manifests []struct { + Digest string `json:"digest"` + Platform struct { + OS string `json:"os"` + Arch string `json:"architecture"` + } `json:"platform"` + } `json:"manifests"` + // v2 image manifest fields + Config struct { + Digest string `json:"digest"` + } `json:"config"` +} + +type registryImageConfig struct { + Config struct { + Cmd []string `json:"Cmd"` + Entrypoint []string `json:"Entrypoint"` + } `json:"config"` +} + +// fetchRegistryToken obtains an anonymous Bearer token for the given repository. +// authURL is the base URL of the auth service (e.g. https://auth.docker.io). +// registryURL is used to derive the service name expected by the auth endpoint. +func fetchRegistryToken(namespace, name, authURL, registryURL string) (string, error) { + // Derive the service name from the registry host (strip scheme). + registryHost := strings.TrimPrefix(strings.TrimPrefix(registryURL, "https://"), "http://") + url := fmt.Sprintf( + "%s/token?service=%s&scope=repository:%s/%s:pull", + authURL, registryHost, namespace, name) + var t registryTokenResponse + if err := fetchJSON(url, &t); err != nil { + return "", err + } + return t.Token, nil +} + +// fetchRegistryJSON performs an authenticated GET against the Docker registry and +// decodes the JSON body into out. The Accept header covers manifest lists, OCI +// indexes, v2 manifests and OCI manifests so that multi-arch tags are handled. +func fetchRegistryJSON(url, token string, out interface{}) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", + "application/vnd.docker.distribution.manifest.list.v2+json,"+ + "application/vnd.oci.image.index.v1+json,"+ + "application/vnd.docker.distribution.manifest.v2+json,"+ + "application/vnd.oci.image.manifest.v1+json") + resp, err := registryClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return json.Unmarshal(body, out) +} + +// fetchImageCommand returns the default startup command for the given image tag +// by walking manifest → (manifest list pick) → config blob. +// Returns "" on any error so callers can treat it as best-effort. +func fetchImageCommand(spec DockerImageSpec, tagName, token, registryURL string) string { + if token == "" { + return "" + } + manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", + registryURL, spec.Namespace, spec.Name, tagName) + + var top registryManifestOrList + if err := fetchRegistryJSON(manifestURL, token, &top); err != nil { + return "" + } + + configDigest := top.Config.Digest + + // Multi-arch: pick linux/amd64, fall back to first linux entry, then first entry. + if len(top.Manifests) > 0 { + chosen := top.Manifests[0].Digest + for _, m := range top.Manifests { + if m.Platform.OS == "linux" { + chosen = m.Digest + if m.Platform.Arch == "amd64" { + break + } + } + } + archManifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", + registryURL, spec.Namespace, spec.Name, chosen) + var archManifest registryManifestOrList + if err := fetchRegistryJSON(archManifestURL, token, &archManifest); err != nil { + return "" + } + configDigest = archManifest.Config.Digest + } + + if configDigest == "" { + return "" + } + + blobURL := fmt.Sprintf("%s/v2/%s/%s/blobs/%s", + registryURL, spec.Namespace, spec.Name, configDigest) + var imgCfg registryImageConfig + if err := fetchRegistryJSON(blobURL, token, &imgCfg); err != nil { + return "" + } + + return cmdSliceToString(imgCfg.Config.Cmd) +} + +// cmdSliceToString converts a Docker CMD slice to a human-readable command string. +// Shell-form CMD (["/bin/sh", "-c", "actual command"]) is unwrapped to its inner +// string; exec-form CMD is joined with spaces. +func cmdSliceToString(cmd []string) string { + if len(cmd) == 0 { + return "" + } + if len(cmd) == 3 && cmd[1] == "-c" && (cmd[0] == "/bin/sh" || cmd[0] == "sh") { + return cmd[2] + } + return strings.Join(cmd, " ") +} + // reMarkdownImage matches the first Markdown image in a string, e.g. ![logo](https://…) var reMarkdownImage = regexp.MustCompile(`!\[[^\]]*\]\((https?://[^)]+)\)`) @@ -465,6 +631,19 @@ func fetchJSON(url string, out interface{}) error { // ─── Entry point ────────────────────────────────────────────────────────────── +// checkHubConnectivity returns true if the Docker Hub API is reachable. +// It uses a short timeout so a missing network path fails fast rather than +// blocking the whole startup sequence. +func checkHubConnectivity(hubURL string) bool { + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(hubURL + "/v2/") + if err != nil { + return false + } + resp.Body.Close() + return true // even a 401/404 means the endpoint is reachable +} + // StartDockerScraper starts the background Docker Hub scraper goroutine. // It runs a full scrape immediately at startup, then repeats on the configured // interval. This function blocks forever and is designed to be called via @@ -475,8 +654,12 @@ func StartDockerScraper() { fmt.Println("[docker-scraper] disabled (DOCKER_SCRAPER_ENABLED=false)") return } - fmt.Printf("[docker-scraper] started — images=%d maxTags=%d interval=%dh\n", - len(cfg.Images), cfg.MaxTags, cfg.IntervalHours) + if !checkHubConnectivity(cfg.HubURL) { + fmt.Printf("[docker-scraper] disabled — %s is not reachable (set DOCKER_SCRAPER_ENABLED=false to suppress)\n", cfg.HubURL) + return + } + fmt.Printf("[docker-scraper] started — images=%d maxTags=%d interval=%dh hub=%s\n", + len(cfg.Images), cfg.MaxTags, cfg.IntervalHours, cfg.HubURL) runScrape(cfg) @@ -491,7 +674,7 @@ func StartDockerScraper() { func runScrape(cfg scraperConfig) { fmt.Printf("[docker-scraper] cycle started at %s\n", time.Now().Format(time.RFC3339)) for _, spec := range cfg.Images { - if err := scrapeImage(spec, cfg.MaxTags); err != nil { + if err := scrapeImage(spec, cfg); err != nil { fmt.Printf("[docker-scraper] %s/%s: %v\n", spec.Namespace, spec.Name, err) } } @@ -503,19 +686,18 @@ func runScrape(cfg scraperConfig) { // scrapeImage fetches Docker Hub metadata for one repository, then either creates // a new ProcessingResource in the catalog or extends the existing one with any // missing tag-instances. -func scrapeImage(spec DockerImageSpec, maxTags int) error { +func scrapeImage(spec DockerImageSpec, cfg scraperConfig) error { // ── Fetch image metadata ────────────────────────────────────────────────── var info hubRepoInfo - repoURL := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/%s/", - spec.Namespace, spec.Name) + repoURL := fmt.Sprintf("%s/v2/repositories/%s/%s/", cfg.HubURL, spec.Namespace, spec.Name) if err := fetchJSON(repoURL, &info); err != nil { return fmt.Errorf("fetch repo info: %w", err) } // ── Fetch tags ──────────────────────────────────────────────────────────── tagsURL := fmt.Sprintf( - "https://hub.docker.com/v2/repositories/%s/%s/tags?page_size=%d&ordering=last_updated", - spec.Namespace, spec.Name, maxTags) + "%s/v2/repositories/%s/%s/tags?page_size=%d&ordering=last_updated", + cfg.HubURL, spec.Namespace, spec.Name, cfg.MaxTags) var tagsResp hubTagsResponse if err := fetchJSON(tagsURL, &tagsResp); err != nil { return fmt.Errorf("fetch tags: %w", err) @@ -524,6 +706,13 @@ func scrapeImage(spec DockerImageSpec, maxTags int) error { return nil // nothing to upsert } + // Pre-fetch a registry token once per image so all tags share it. + token, _ := fetchRegistryToken(spec.Namespace, spec.Name, cfg.AuthURL, cfg.RegistryURL) + cmds := make(map[string]string, len(tagsResp.Results)) + for _, t := range tagsResp.Results { + cmds[t.Name] = fetchImageCommand(spec, t.Name, token, cfg.RegistryURL) + } + adminReq := &tools.APIRequest{Admin: true} accessor := (&resources.ProcessingResource{}).GetAccessor(adminReq) @@ -531,9 +720,9 @@ func scrapeImage(spec DockerImageSpec, maxTags int) error { existing := findProcessingResourceByName(accessor, resourceName) if existing == nil { - return createDockerProcessingResource(accessor, spec, resourceName, info, tagsResp.Results) + return createDockerProcessingResource(accessor, spec, resourceName, info, tagsResp.Results, cmds) } - return syncDockerInstances(accessor, existing, spec, tagsResp.Results) + return syncDockerInstances(accessor, existing, spec, tagsResp.Results, cmds) } // resourceName returns the canonical catalog name for a DockerImageSpec. @@ -589,6 +778,7 @@ func createDockerProcessingResource( name string, info hubRepoInfo, tags []hubTag, + cmds map[string]string, ) error { resource := &resources.ProcessingResource{ AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ProcessingInstance]{ @@ -611,7 +801,7 @@ func createDockerProcessingResource( } for i := range tags { - resource.AddInstances(buildPeerlessInstance(spec, tags[i])) + resource.AddInstances(buildPeerlessInstance(spec, tags[i], cmds[tags[i].Name])) } // StoreOne goes through GenericStoreOne which calls AbstractResource.StoreDraftDefault() @@ -637,6 +827,7 @@ func syncDockerInstances( resource *resources.ProcessingResource, spec DockerImageSpec, tags []hubTag, + cmds map[string]string, ) error { existing := map[string]bool{} for _, inst := range resource.Instances { @@ -649,7 +840,7 @@ func syncDockerInstances( if existing[ref] { continue } - resource.AddInstances(buildPeerlessInstance(spec, tags[i])) + resource.AddInstances(buildPeerlessInstance(spec, tags[i], cmds[tags[i].Name])) added++ } if added == 0 { @@ -673,15 +864,8 @@ func syncDockerInstances( // Origin.Ref != "" (set to the canonical docker pull reference) // // ProcessingInstance.StoreDraftDefault() enforces this invariant on write. -func buildPeerlessInstance(spec DockerImageSpec, tag hubTag) *resources.ProcessingInstance { +func buildPeerlessInstance(spec DockerImageSpec, tag hubTag, cmd string) *resources.ProcessingInstance { ref := dockerRef(spec, tag.Name) - - // Collect architecture hint from the first image manifest entry (if any). - arch := "" - if len(tag.Images) > 0 { - arch = tag.Images[0].Architecture - } - return &resources.ProcessingInstance{ ResourceInstance: resources.ResourceInstance[*resources.ResourcePartnerShip[*resources.ProcessingResourcePricingProfile]]{ AbstractObject: utils.AbstractObject{ @@ -699,11 +883,8 @@ func buildPeerlessInstance(spec DockerImageSpec, tag hubTag) *resources.Processi }, Access: &resources.ResourceAccess{ Container: &models.Container{ - Image: ref, - // Command, Args, Env, Volumes left empty — image defaults apply. - Env: map[string]string{ - "ARCH": arch, - }, + Image: ref, + Command: cmd, // default CMD from the image config (best-effort, "" if unavailable) }, }, } diff --git a/infrastructure/nats.go b/infrastructure/nats.go index 350376e..590a810 100644 --- a/infrastructure/nats.go +++ b/infrastructure/nats.go @@ -31,9 +31,7 @@ func EmitNATS(user string, groups []string, message tools.PropalgationMessage) { b, _ := json.Marshal(message) switch message.Action { case tools.PB_SEARCH: - fmt.Println("EMITNATS") SearchMu.Lock() - fmt.Println("afterlock") SearchStream[user] = make(chan []byte, 128) SearchStreamSeen[user] = []string{} SearchStreamExtend[user] = []string{} @@ -88,8 +86,6 @@ func ListenNATS() { "dtype": p.GetType(), "data": p, }) - fmt.Println("WRAPPED CHECK") - SearchMu.Lock() if a := SearchStreamExtend[resp.User]; len(a) > 0 { wrapped, merr = json.Marshal(map[string]interface{}{ @@ -106,7 +102,6 @@ func ListenNATS() { } ch := SearchStream[resp.User] SearchMu.Unlock() - fmt.Println(resp.User, ch, merr, alreadySeen) if merr != nil || alreadySeen { return } diff --git a/oc-catalog b/oc-catalog new file mode 100755 index 0000000..2f89929 Binary files /dev/null and b/oc-catalog differ diff --git a/routers/router.go b/routers/router.go index e6bd4d5..6172fae 100755 --- a/routers/router.go +++ b/routers/router.go @@ -54,7 +54,6 @@ func TypedSearchHandler(w http.ResponseWriter, r *http.Request) { } user, _, groups := oclib.ExtractTokenInfoWs(*r) b, _ := json.Marshal(map[string]string{"search": search, "type": t}) - fmt.Println("SENDE", user, search) infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{ Action: tools.PB_SEARCH, DataType: dataType, diff --git a/swagger/swagger.json b/swagger/swagger.json index d1681e4..9e99155 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -447,6 +447,13 @@ "description": "search workspace\n\u003cbr\u003e", "operationId": "ResourceController.Search", "parameters": [ + { + "in": "path", + "name": "type", + "description": "the type you want to get", + "required": true, + "type": "string" + }, { "in": "query", "name": "is_draft", diff --git a/swagger/swagger.yml b/swagger/swagger.yml index c18fe5e..4597350 100644 --- a/swagger/swagger.yml +++ b/swagger/swagger.yml @@ -419,6 +419,11 @@ paths:
operationId: ResourceController.Search parameters: + - in: path + name: type + description: the type you want to get + required: true + type: string - in: query name: is_draft description: "false" diff --git a/ws.go b/ws.go index f0ede93..3954fdc 100644 --- a/ws.go +++ b/ws.go @@ -89,7 +89,6 @@ func main() { var data any if err := json.Unmarshal([]byte(raw), &data); err == nil { ///b, _ := json.MarshalIndent(data, "", " ") - fmt.Println(data) } else { fmt.Printf("Message brut : %s\n", raw, err) }