@@ -63,6 +63,7 @@ const (
6363 ociImageIndexName = "ec.oci.image_index"
6464 ociImageTagRefsName = "ec.oci.image_tag_refs"
6565 ociImageReferrersName = "ec.oci.image_referrers"
66+ ociParsedBlobName = "ec.oci.parsed_blob"
6667 maxTarEntrySizeConst = 500 * 1024 * 1024 // 500MB
6768)
6869
@@ -96,6 +97,28 @@ func registerOCIBlob() {
9697 })
9798}
9899
100+ func registerOCIParsedBlob () {
101+ decl := rego.Function {
102+ Name : ociParsedBlobName ,
103+ Decl : types .NewFunction (
104+ types .Args (
105+ types .Named ("ref" , types .S ).Description ("OCI blob reference" ),
106+ ),
107+ types .Named ("value" , types .A ).Description ("the parsed JSON value from the OCI blob" ),
108+ ),
109+ Memoize : true ,
110+ Nondeterministic : true ,
111+ }
112+
113+ rego .RegisterBuiltin1 (& decl , ociParsedBlob )
114+ ast .RegisterBuiltin (& ast.Builtin {
115+ Name : decl .Name ,
116+ Description : "Fetch a blob from an OCI registry and return the parsed JSON value. Results are cached per component." ,
117+ Decl : decl .Decl ,
118+ Nondeterministic : decl .Nondeterministic ,
119+ })
120+ }
121+
99122func registerOCIDescriptor () {
100123 platform := types .NewObject (
101124 []* types.StaticProperty {
@@ -603,6 +626,71 @@ func ociBlobInternal(bctx rego.BuiltinContext, a *ast.Term, verifyDigest bool) (
603626 return result .(* ast.Term ), nil
604627}
605628
629+ func ociParsedBlob (bctx rego.BuiltinContext , a * ast.Term ) (* ast.Term , error ) {
630+ logger := log .WithField ("function" , ociParsedBlobName )
631+
632+ uri , ok := a .Value .(ast.String )
633+ if ! ok {
634+ logger .Error ("input is not a string" )
635+ return nil , nil
636+ }
637+ refStr := string (uri )
638+ logger = logger .WithField ("ref" , refStr )
639+
640+ cc := componentCacheFromContext (bctx .Context )
641+
642+ if cached , found := cc .parsedBlobCache .Load (refStr ); found {
643+ logger .Debug ("Parsed blob served from cache" )
644+ return cached .(* ast.Term ), nil
645+ }
646+
647+ result , err , _ := cc .parsedBlobFlight .Do (refStr , func () (any , error ) {
648+ if cached , found := cc .parsedBlobCache .Load (refStr ); found {
649+ logger .Debug ("Parsed blob served from cache (after singleflight)" )
650+ return cached , nil
651+ }
652+
653+ rawTerm , err := ociBlobInternal (bctx , a , true )
654+ if err != nil || rawTerm == nil {
655+ return nil , nil //nolint:nilerr
656+ }
657+
658+ rawStr , ok := rawTerm .Value .(ast.String )
659+ if ! ok {
660+ logger .Error ("blob value is not a string" )
661+ return nil , nil //nolint:nilerr
662+ }
663+
664+ var parsed any
665+ if err := json .Unmarshal ([]byte (string (rawStr )), & parsed ); err != nil {
666+ logger .WithFields (log.Fields {
667+ "action" : "unmarshal" ,
668+ "error" : err ,
669+ }).Error ("failed to unmarshal blob as JSON" )
670+ return nil , nil //nolint:nilerr
671+ }
672+
673+ value , err := ast .InterfaceToValue (parsed )
674+ if err != nil {
675+ logger .WithFields (log.Fields {
676+ "action" : "convert to ast" ,
677+ "error" : err ,
678+ }).Error ("failed to convert parsed JSON to AST value" )
679+ return nil , nil //nolint:nilerr
680+ }
681+
682+ term := ast .NewTerm (value )
683+ cc .parsedBlobCache .Store (refStr , term )
684+ logger .Debug ("Parsed blob cached" )
685+ return term , nil
686+ })
687+
688+ if err != nil || result == nil {
689+ return nil , nil
690+ }
691+ return result .(* ast.Term ), nil
692+ }
693+
606694func ociDescriptor (bctx rego.BuiltinContext , a * ast.Term ) (* ast.Term , error ) {
607695 logger := log .WithField ("function" , ociDescriptorName )
608696
@@ -807,10 +895,12 @@ func ClearCaches() {
807895// Lighter caches (manifests, descriptors, image indexes) remain global because they
808896// are small and benefit from cross-component sharing (e.g., shared task bundle manifests).
809897type ComponentCache struct {
810- blobCache sync.Map
811- blobFlight singleflight.Group
812- filesCache sync.Map
813- filesFlight singleflight.Group
898+ blobCache sync.Map
899+ blobFlight singleflight.Group
900+ filesCache sync.Map
901+ filesFlight singleflight.Group
902+ parsedBlobCache sync.Map
903+ parsedBlobFlight singleflight.Group
814904}
815905
816906type componentCacheKey struct {}
@@ -1634,6 +1724,7 @@ func isNotFoundError(err error) bool {
16341724
16351725func init () {
16361726 registerOCIBlob ()
1727+ registerOCIParsedBlob ()
16371728 registerOCIBlobFiles ()
16381729 registerOCIDescriptor ()
16391730 registerOCIImageFiles ()
0 commit comments