make ci catalog
This commit is contained in:
@@ -43,7 +43,8 @@ func (o *GeneralController) GetAll() {
|
|||||||
Groups: groups,
|
Groups: groups,
|
||||||
Admin: true,
|
Admin: true,
|
||||||
}
|
}
|
||||||
newWorkflow, err = newWorkflow.ExtractFromPlantUML(file, req)
|
var importWarnings []string
|
||||||
|
newWorkflow, importWarnings, err = newWorkflow.ExtractFromPlantUML(file, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.Data["json"] = map[string]interface{}{
|
o.Data["json"] = map[string]interface{}{
|
||||||
"data": nil,
|
"data": nil,
|
||||||
@@ -58,6 +59,7 @@ func (o *GeneralController) GetAll() {
|
|||||||
"data": newWorkflow,
|
"data": newWorkflow,
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"error": nil,
|
"error": nil,
|
||||||
|
"warnings": importWarnings,
|
||||||
}
|
}
|
||||||
|
|
||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
@@ -93,7 +95,6 @@ func Websocket(ctx context.Context, user string, groups []string, dataType int,
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case msg, ok := <-infrastructure.SearchStream[user]:
|
case msg, ok := <-infrastructure.SearchStream[user]:
|
||||||
fmt.Println("msg", msg, ok)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-6
@@ -113,12 +113,12 @@ func (o *ResourceController) Search() {
|
|||||||
limit, _ := strconv.Atoi(o.Ctx.Input.Query("limit"))
|
limit, _ := strconv.Atoi(o.Ctx.Input.Query("limit"))
|
||||||
|
|
||||||
m := map[string][]utils.ShallowDBObject{}
|
m := map[string][]utils.ShallowDBObject{}
|
||||||
fmt.Println(o.collection(false))
|
|
||||||
for _, col := range o.collection(false) {
|
for _, col := range o.collection(false) {
|
||||||
if m[col.String()] == nil {
|
if m[col.String()] == nil {
|
||||||
m[col.String()] = []utils.ShallowDBObject{}
|
m[col.String()] = []utils.ShallowDBObject{}
|
||||||
}
|
}
|
||||||
s := oclib.NewRequest(col, user, peerID, groups, nil).Search(nil, search, isDraft == "true", int64(offset), int64(limit))
|
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...)
|
m[col.String()] = append(m[col.String()], s.Data...)
|
||||||
}
|
}
|
||||||
mm := map[string]interface{}{}
|
mm := map[string]interface{}{}
|
||||||
@@ -154,6 +154,7 @@ func GetResource(typ oclib.LibDataEnum) interface{} {
|
|||||||
|
|
||||||
// @Title Search
|
// @Title Search
|
||||||
// @Description search workspace
|
// @Description search workspace
|
||||||
|
// @Param type path string true "the type you want to get"
|
||||||
// @Param is_draft query string false
|
// @Param is_draft query string false
|
||||||
// @Param extend query string false "extend"
|
// @Param extend query string false "extend"
|
||||||
// @Param offset query string false
|
// @Param offset query string false
|
||||||
@@ -162,6 +163,7 @@ func GetResource(typ oclib.LibDataEnum) interface{} {
|
|||||||
// @Success 200 {workspace} models.workspace
|
// @Success 200 {workspace} models.workspace
|
||||||
// @router /:type/extended/search [post]
|
// @router /:type/extended/search [post]
|
||||||
func (o *ResourceController) SearchExtended() {
|
func (o *ResourceController) SearchExtended() {
|
||||||
|
fmt.Println("THERE")
|
||||||
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
|
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
|
||||||
// store and return Id or post with UUIDLibDataEnum
|
// store and return Id or post with UUIDLibDataEnum
|
||||||
isDraft := o.Ctx.Input.Query("is_draft")
|
isDraft := o.Ctx.Input.Query("is_draft")
|
||||||
@@ -175,6 +177,7 @@ func (o *ResourceController) SearchExtended() {
|
|||||||
if m[col.String()] == nil {
|
if m[col.String()] == nil {
|
||||||
m[col.String()] = []utils.ShallowDBObject{}
|
m[col.String()] = []utils.ShallowDBObject{}
|
||||||
}
|
}
|
||||||
|
fmt.Println("filters", oclib.FiltersFromFlatMap(res, GetResource(col)))
|
||||||
s := oclib.NewRequest(col, user, peerID, groups, nil).Search(
|
s := oclib.NewRequest(col, user, peerID, groups, nil).Search(
|
||||||
oclib.FiltersFromFlatMap(res, GetResource(col)), "", isDraft == "true", int64(offset), int64(limit))
|
oclib.FiltersFromFlatMap(res, GetResource(col)), "", isDraft == "true", int64(offset), int64(limit))
|
||||||
m[col.String()] = append(m[col.String()], s.Data...)
|
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)
|
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
|
||||||
id := o.Ctx.Input.Param(":id")
|
id := o.Ctx.Input.Param(":id")
|
||||||
extend := strings.Split(o.Ctx.Input.Query("extend"), ",")
|
extend := strings.Split(o.Ctx.Input.Query("extend"), ",")
|
||||||
fmt.Println("COLLECTIOn", o.collection(false))
|
|
||||||
for _, col := range o.collection(false) {
|
for _, col := range o.collection(false) {
|
||||||
if d := oclib.NewRequest(col, user, peerID, groups, nil).LoadOne(id); d.Data != nil {
|
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{}{})
|
o.Data["json"] = oclib.GetExtend(d.Data, d.Data.Extend(extend...), map[tools.DataType]map[string]interface{}{})
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
@@ -235,12 +236,23 @@ func (o *ResourceController) Post() {
|
|||||||
var res map[string]interface{}
|
var res map[string]interface{}
|
||||||
json.Unmarshal(o.Ctx.Input.CopyBody(100000), &res)
|
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
|
// 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.
|
// avant la persistance : le vrai path ne doit jamais sortir de ce peer.
|
||||||
replacePrivateSources(res)
|
replacePrivateSources(res)
|
||||||
|
|
||||||
data := oclib.NewRequest(libs[0], user, peerID, groups, nil).StoreOne(res)
|
data := oclib.NewRequest(libs[0], user, peerID, groups, nil).StoreOne(res)
|
||||||
fmt.Println(data.Data, res["name"], libs[0])
|
|
||||||
if data.Err == "" {
|
if data.Err == "" {
|
||||||
payload, _ := json.Marshal(data.Data.Serialize(data.Data))
|
payload, _ := json.Marshal(data.Data.Serialize(data.Data))
|
||||||
infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{
|
infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{
|
||||||
@@ -338,12 +350,10 @@ func (o *ResourceController) Put() {
|
|||||||
// @Success 200 {resource} models.resource
|
// @Success 200 {resource} models.resource
|
||||||
// @router /:type/:id [delete]
|
// @router /:type/:id [delete]
|
||||||
func (o *ResourceController) Delete() {
|
func (o *ResourceController) Delete() {
|
||||||
fmt.Println("THERE")
|
|
||||||
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
|
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request)
|
||||||
id := o.Ctx.Input.Param(":id")
|
id := o.Ctx.Input.Param(":id")
|
||||||
for _, col := range o.collection(false) {
|
for _, col := range o.collection(false) {
|
||||||
data := oclib.NewRequest(col, user, peerID, groups, nil).DeleteOne(id)
|
data := oclib.NewRequest(col, user, peerID, groups, nil).DeleteOne(id)
|
||||||
fmt.Println(col, data, id)
|
|
||||||
if data.Err == "" {
|
if data.Err == "" {
|
||||||
o.Data["json"] = data
|
o.Data["json"] = data
|
||||||
payload, _ := json.Marshal(data.Data.Serialize(data.Data))
|
payload, _ := json.Marshal(data.Data.Serialize(data.Data))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module oc-catalog
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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/beego/beego/v2 v2.3.8
|
||||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||||
github.com/smartystreets/goconvey v1.7.2
|
github.com/smartystreets/goconvey v1.7.2
|
||||||
|
|||||||
@@ -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-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 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-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/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 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ package infrastructure
|
|||||||
// Default: a curated set of popular official images.
|
// Default: a curated set of popular official images.
|
||||||
// DOCKER_SCRAPER_MAX_TAGS max tags to import per image (default: 10)
|
// DOCKER_SCRAPER_MAX_TAGS max tags to import per image (default: 10)
|
||||||
// DOCKER_SCRAPER_INTERVAL_H refresh interval in hours (default: 24)
|
// 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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -56,6 +65,9 @@ type scraperConfig struct {
|
|||||||
Images []DockerImageSpec
|
Images []DockerImageSpec
|
||||||
MaxTags int
|
MaxTags int
|
||||||
IntervalHours 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.
|
// 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
|
// scraperConfigFromEnv reads scraper configuration from environment variables
|
||||||
// and returns a populated scraperConfig with sensible defaults.
|
// and returns a populated scraperConfig with sensible defaults.
|
||||||
func scraperConfigFromEnv() scraperConfig {
|
func scraperConfigFromEnv() scraperConfig {
|
||||||
|
envOrDefault := func(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return strings.TrimRight(v, "/")
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
cfg := scraperConfig{
|
cfg := scraperConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
MaxTags: 10,
|
MaxTags: 10,
|
||||||
IntervalHours: 24,
|
IntervalHours: 24,
|
||||||
Images: defaultImages,
|
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" {
|
if v := os.Getenv("DOCKER_SCRAPER_ENABLED"); v == "false" {
|
||||||
@@ -306,6 +328,150 @@ type hubTagsResponse struct {
|
|||||||
Results []hubTag `json:"results"`
|
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. 
|
// reMarkdownImage matches the first Markdown image in a string, e.g. 
|
||||||
var reMarkdownImage = regexp.MustCompile(`!\[[^\]]*\]\((https?://[^)]+)\)`)
|
var reMarkdownImage = regexp.MustCompile(`!\[[^\]]*\]\((https?://[^)]+)\)`)
|
||||||
|
|
||||||
@@ -465,6 +631,19 @@ func fetchJSON(url string, out interface{}) error {
|
|||||||
|
|
||||||
// ─── Entry point ──────────────────────────────────────────────────────────────
|
// ─── 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.
|
// StartDockerScraper starts the background Docker Hub scraper goroutine.
|
||||||
// It runs a full scrape immediately at startup, then repeats on the configured
|
// 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
|
// 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)")
|
fmt.Println("[docker-scraper] disabled (DOCKER_SCRAPER_ENABLED=false)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("[docker-scraper] started — images=%d maxTags=%d interval=%dh\n",
|
if !checkHubConnectivity(cfg.HubURL) {
|
||||||
len(cfg.Images), cfg.MaxTags, cfg.IntervalHours)
|
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)
|
runScrape(cfg)
|
||||||
|
|
||||||
@@ -491,7 +674,7 @@ func StartDockerScraper() {
|
|||||||
func runScrape(cfg scraperConfig) {
|
func runScrape(cfg scraperConfig) {
|
||||||
fmt.Printf("[docker-scraper] cycle started at %s\n", time.Now().Format(time.RFC3339))
|
fmt.Printf("[docker-scraper] cycle started at %s\n", time.Now().Format(time.RFC3339))
|
||||||
for _, spec := range cfg.Images {
|
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)
|
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
|
// scrapeImage fetches Docker Hub metadata for one repository, then either creates
|
||||||
// a new ProcessingResource in the catalog or extends the existing one with any
|
// a new ProcessingResource in the catalog or extends the existing one with any
|
||||||
// missing tag-instances.
|
// missing tag-instances.
|
||||||
func scrapeImage(spec DockerImageSpec, maxTags int) error {
|
func scrapeImage(spec DockerImageSpec, cfg scraperConfig) error {
|
||||||
// ── Fetch image metadata ──────────────────────────────────────────────────
|
// ── Fetch image metadata ──────────────────────────────────────────────────
|
||||||
var info hubRepoInfo
|
var info hubRepoInfo
|
||||||
repoURL := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/%s/",
|
repoURL := fmt.Sprintf("%s/v2/repositories/%s/%s/", cfg.HubURL, spec.Namespace, spec.Name)
|
||||||
spec.Namespace, spec.Name)
|
|
||||||
if err := fetchJSON(repoURL, &info); err != nil {
|
if err := fetchJSON(repoURL, &info); err != nil {
|
||||||
return fmt.Errorf("fetch repo info: %w", err)
|
return fmt.Errorf("fetch repo info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fetch tags ────────────────────────────────────────────────────────────
|
// ── Fetch tags ────────────────────────────────────────────────────────────
|
||||||
tagsURL := fmt.Sprintf(
|
tagsURL := fmt.Sprintf(
|
||||||
"https://hub.docker.com/v2/repositories/%s/%s/tags?page_size=%d&ordering=last_updated",
|
"%s/v2/repositories/%s/%s/tags?page_size=%d&ordering=last_updated",
|
||||||
spec.Namespace, spec.Name, maxTags)
|
cfg.HubURL, spec.Namespace, spec.Name, cfg.MaxTags)
|
||||||
var tagsResp hubTagsResponse
|
var tagsResp hubTagsResponse
|
||||||
if err := fetchJSON(tagsURL, &tagsResp); err != nil {
|
if err := fetchJSON(tagsURL, &tagsResp); err != nil {
|
||||||
return fmt.Errorf("fetch tags: %w", err)
|
return fmt.Errorf("fetch tags: %w", err)
|
||||||
@@ -524,6 +706,13 @@ func scrapeImage(spec DockerImageSpec, maxTags int) error {
|
|||||||
return nil // nothing to upsert
|
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}
|
adminReq := &tools.APIRequest{Admin: true}
|
||||||
accessor := (&resources.ProcessingResource{}).GetAccessor(adminReq)
|
accessor := (&resources.ProcessingResource{}).GetAccessor(adminReq)
|
||||||
|
|
||||||
@@ -531,9 +720,9 @@ func scrapeImage(spec DockerImageSpec, maxTags int) error {
|
|||||||
existing := findProcessingResourceByName(accessor, resourceName)
|
existing := findProcessingResourceByName(accessor, resourceName)
|
||||||
|
|
||||||
if existing == nil {
|
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.
|
// resourceName returns the canonical catalog name for a DockerImageSpec.
|
||||||
@@ -589,6 +778,7 @@ func createDockerProcessingResource(
|
|||||||
name string,
|
name string,
|
||||||
info hubRepoInfo,
|
info hubRepoInfo,
|
||||||
tags []hubTag,
|
tags []hubTag,
|
||||||
|
cmds map[string]string,
|
||||||
) error {
|
) error {
|
||||||
resource := &resources.ProcessingResource{
|
resource := &resources.ProcessingResource{
|
||||||
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ProcessingInstance]{
|
AbstractInstanciatedResource: resources.AbstractInstanciatedResource[*resources.ProcessingInstance]{
|
||||||
@@ -611,7 +801,7 @@ func createDockerProcessingResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range tags {
|
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()
|
// StoreOne goes through GenericStoreOne which calls AbstractResource.StoreDraftDefault()
|
||||||
@@ -637,6 +827,7 @@ func syncDockerInstances(
|
|||||||
resource *resources.ProcessingResource,
|
resource *resources.ProcessingResource,
|
||||||
spec DockerImageSpec,
|
spec DockerImageSpec,
|
||||||
tags []hubTag,
|
tags []hubTag,
|
||||||
|
cmds map[string]string,
|
||||||
) error {
|
) error {
|
||||||
existing := map[string]bool{}
|
existing := map[string]bool{}
|
||||||
for _, inst := range resource.Instances {
|
for _, inst := range resource.Instances {
|
||||||
@@ -649,7 +840,7 @@ func syncDockerInstances(
|
|||||||
if existing[ref] {
|
if existing[ref] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resource.AddInstances(buildPeerlessInstance(spec, tags[i]))
|
resource.AddInstances(buildPeerlessInstance(spec, tags[i], cmds[tags[i].Name]))
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
if added == 0 {
|
if added == 0 {
|
||||||
@@ -673,15 +864,8 @@ func syncDockerInstances(
|
|||||||
// Origin.Ref != "" (set to the canonical docker pull reference)
|
// Origin.Ref != "" (set to the canonical docker pull reference)
|
||||||
//
|
//
|
||||||
// ProcessingInstance.StoreDraftDefault() enforces this invariant on write.
|
// 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)
|
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{
|
return &resources.ProcessingInstance{
|
||||||
ResourceInstance: resources.ResourceInstance[*resources.ResourcePartnerShip[*resources.ProcessingResourcePricingProfile]]{
|
ResourceInstance: resources.ResourceInstance[*resources.ResourcePartnerShip[*resources.ProcessingResourcePricingProfile]]{
|
||||||
AbstractObject: utils.AbstractObject{
|
AbstractObject: utils.AbstractObject{
|
||||||
@@ -700,10 +884,7 @@ func buildPeerlessInstance(spec DockerImageSpec, tag hubTag) *resources.Processi
|
|||||||
Access: &resources.ResourceAccess{
|
Access: &resources.ResourceAccess{
|
||||||
Container: &models.Container{
|
Container: &models.Container{
|
||||||
Image: ref,
|
Image: ref,
|
||||||
// Command, Args, Env, Volumes left empty — image defaults apply.
|
Command: cmd, // default CMD from the image config (best-effort, "" if unavailable)
|
||||||
Env: map[string]string{
|
|
||||||
"ARCH": arch,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ func EmitNATS(user string, groups []string, message tools.PropalgationMessage) {
|
|||||||
b, _ := json.Marshal(message)
|
b, _ := json.Marshal(message)
|
||||||
switch message.Action {
|
switch message.Action {
|
||||||
case tools.PB_SEARCH:
|
case tools.PB_SEARCH:
|
||||||
fmt.Println("EMITNATS")
|
|
||||||
SearchMu.Lock()
|
SearchMu.Lock()
|
||||||
fmt.Println("afterlock")
|
|
||||||
SearchStream[user] = make(chan []byte, 128)
|
SearchStream[user] = make(chan []byte, 128)
|
||||||
SearchStreamSeen[user] = []string{}
|
SearchStreamSeen[user] = []string{}
|
||||||
SearchStreamExtend[user] = []string{}
|
SearchStreamExtend[user] = []string{}
|
||||||
@@ -88,8 +86,6 @@ func ListenNATS() {
|
|||||||
"dtype": p.GetType(),
|
"dtype": p.GetType(),
|
||||||
"data": p,
|
"data": p,
|
||||||
})
|
})
|
||||||
fmt.Println("WRAPPED CHECK")
|
|
||||||
|
|
||||||
SearchMu.Lock()
|
SearchMu.Lock()
|
||||||
if a := SearchStreamExtend[resp.User]; len(a) > 0 {
|
if a := SearchStreamExtend[resp.User]; len(a) > 0 {
|
||||||
wrapped, merr = json.Marshal(map[string]interface{}{
|
wrapped, merr = json.Marshal(map[string]interface{}{
|
||||||
@@ -106,7 +102,6 @@ func ListenNATS() {
|
|||||||
}
|
}
|
||||||
ch := SearchStream[resp.User]
|
ch := SearchStream[resp.User]
|
||||||
SearchMu.Unlock()
|
SearchMu.Unlock()
|
||||||
fmt.Println(resp.User, ch, merr, alreadySeen)
|
|
||||||
if merr != nil || alreadySeen {
|
if merr != nil || alreadySeen {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -54,7 +54,6 @@ func TypedSearchHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
user, _, groups := oclib.ExtractTokenInfoWs(*r)
|
user, _, groups := oclib.ExtractTokenInfoWs(*r)
|
||||||
b, _ := json.Marshal(map[string]string{"search": search, "type": t})
|
b, _ := json.Marshal(map[string]string{"search": search, "type": t})
|
||||||
fmt.Println("SENDE", user, search)
|
|
||||||
infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{
|
infrastructure.EmitNATS(user, groups, tools.PropalgationMessage{
|
||||||
Action: tools.PB_SEARCH,
|
Action: tools.PB_SEARCH,
|
||||||
DataType: dataType,
|
DataType: dataType,
|
||||||
|
|||||||
@@ -447,6 +447,13 @@
|
|||||||
"description": "search workspace\n\u003cbr\u003e",
|
"description": "search workspace\n\u003cbr\u003e",
|
||||||
"operationId": "ResourceController.Search",
|
"operationId": "ResourceController.Search",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "type",
|
||||||
|
"description": "the type you want to get",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"name": "is_draft",
|
"name": "is_draft",
|
||||||
|
|||||||
@@ -419,6 +419,11 @@ paths:
|
|||||||
<br>
|
<br>
|
||||||
operationId: ResourceController.Search
|
operationId: ResourceController.Search
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: type
|
||||||
|
description: the type you want to get
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
- in: query
|
- in: query
|
||||||
name: is_draft
|
name: is_draft
|
||||||
description: "false"
|
description: "false"
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ func main() {
|
|||||||
var data any
|
var data any
|
||||||
if err := json.Unmarshal([]byte(raw), &data); err == nil {
|
if err := json.Unmarshal([]byte(raw), &data); err == nil {
|
||||||
///b, _ := json.MarshalIndent(data, "", " ")
|
///b, _ := json.MarshalIndent(data, "", " ")
|
||||||
fmt.Println(data)
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Message brut : %s\n", raw, err)
|
fmt.Printf("Message brut : %s\n", raw, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user