@@ -35,6 +35,7 @@ const (
3535 helmTemplatePlaceholder = "_aicr_helm_template_"
3636 imageRepositoryKey = "repository"
3737 imageTagKey = "tag"
38+ imageDigestKey = "digest"
3839)
3940
4041var helmTemplateRE = regexp .MustCompile (`\{\{[^{}]*\}\}` )
@@ -72,10 +73,12 @@ func stripHelmTemplates(data []byte) []byte {
7273// mappings. Empty or null scalar `image:` values and values still containing an
7374// unrendered Go template directive are skipped. A recognized structured
7475// descriptor returns an invalid-request error when a present name or
75- // repository field is null, empty, or non-scalar, when a tag field is
76- // non-scalar, or when a registry or digest member is present (dropping either
77- // would emit a wrong or un-pinned reference). A null or empty tag follows the
78- // Helm appVersion idiom and is treated as absent.
76+ // repository field is null, empty, or non-scalar, when a tag or digest field
77+ // is non-scalar, or when a registry member is present (which cannot be folded
78+ // into the reference without losing information). A null or empty tag or digest
79+ // follows the Helm appVersion/unpinned idiom and is treated as absent. A
80+ // non-empty digest is validated as sha256:<64 lowercase hex chars> and folded
81+ // in as an @<digest> suffix.
7982//
8083// Helm template directives ({{ ... }}) are replaced with a placeholder before
8184// parsing, so files mixing YAML with Helm templates (those under
@@ -206,31 +209,40 @@ func isStructuredImageKey(key string) bool {
206209// repository: ghcr.io/kai-scheduler/kai-scheduler
207210// tag: v0.14.1
208211//
212+ // It also handles the digest-pinned form used by Helm charts that separate
213+ // repository, tag, and digest into sibling fields (e.g., Bitnami-style):
214+ //
215+ // image:
216+ // repository: docker.io/library/postgres
217+ // tag: "17.4"
218+ // digest: sha256:304ab813518754228f9f792f79d6da36359b82d8ecf418096c636725f8c930ad
219+ //
209220// Some charts omit name because repository already carries the full image
210221// path. In that form, repository becomes the image name before tag is
211222// appended. A present name or repository must be a non-null, non-empty
212223// scalar. A null or empty tag is the Helm idiom for "default to the chart
213- // appVersion" and is treated like an absent tag. Only name, repository, and
214- // tag are combined; a present registry or digest member is rejected because
215- // dropping it would emit a reference that resolves to the wrong registry or
216- // silently un-pins a digest. Other members (pullPolicy, pullSecrets, ...)
217- // do not affect the reference identity and are ignored.
224+ // appVersion" and is treated like an absent tag. A present digest is appended
225+ // as @<digest> so the extracted reference is fully pinned. A present registry
226+ // member is rejected because dropping it would resolve to the wrong registry.
227+ // Other members (pullPolicy, pullSecrets, ...) do not affect reference
228+ // identity and are ignored.
218229func imageReferenceFromMapping (n * yaml.Node ) (string , error ) {
219- var name , repository , tag string
230+ var name , repository , tag , digest string
220231 var namePresent bool
232+ var digestNode * yaml.Node
221233 for i := 0 ; i + 1 < len (n .Content ); i += 2 {
222234 key , value := n .Content [i ], n .Content [i + 1 ]
223235 switch key .Value {
224- case "name" , imageRepositoryKey , imageTagKey :
225- case "registry" , "digest" :
236+ case "name" , imageRepositoryKey , imageTagKey , imageDigestKey :
237+ case "registry" :
226238 return "" , errors .Wrap (
227239 errors .ErrCodeInvalidRequest ,
228240 "invalid image descriptor member" ,
229241 & invalidStructuredImageDescriptorError {
230242 field : key .Value ,
231243 line : key .Line ,
232244 column : key .Column ,
233- reason : "is not combined into the extracted reference; fold it into repository or tag " ,
245+ reason : "is not combined into the extracted reference; fold it into repository" ,
234246 },
235247 )
236248 default :
@@ -239,10 +251,11 @@ func imageReferenceFromMapping(n *yaml.Node) (string, error) {
239251
240252 scalar , ok := nonNullImageMappingScalar (value )
241253 if ! ok {
242- if key .Value == imageTagKey && isNullOrEmptyScalar (value ) {
243- // tag: "" / tag: null — the Helm "use appVersion"
244- // idiom. Treat like an absent tag instead of failing
245- // the whole survey.
254+ if (key .Value == imageTagKey || key .Value == imageDigestKey ) && isNullOrEmptyScalar (value ) {
255+ // tag: "" / tag: null — the Helm "use appVersion" idiom.
256+ // digest: "" / digest: null — unpinned default in charts
257+ // that optionally carry a digest pin.
258+ // Treat both as absent rather than failing the whole survey.
246259 continue
247260 }
248261 return "" , errors .Wrap (
@@ -264,6 +277,9 @@ func imageReferenceFromMapping(n *yaml.Node) (string, error) {
264277 repository = scalar
265278 case imageTagKey :
266279 tag = scalar
280+ case imageDigestKey :
281+ digest = scalar
282+ digestNode = value
267283 }
268284 }
269285 if ! namePresent {
@@ -276,7 +292,23 @@ func imageReferenceFromMapping(n *yaml.Node) (string, error) {
276292 if name == "" {
277293 return "" , nil
278294 }
279- return combineCRDTriplet (name , repository , tag ), nil
295+ ref := combineCRDTriplet (name , repository , tag )
296+ if digest == "" || strings .Contains (ref , "@" ) {
297+ return ref , nil
298+ }
299+ if ! containerSHARE .MatchString (digest ) {
300+ return "" , errors .Wrap (
301+ errors .ErrCodeInvalidRequest ,
302+ "invalid image descriptor member" ,
303+ & invalidStructuredImageDescriptorError {
304+ field : imageDigestKey ,
305+ line : digestNode .Line ,
306+ column : digestNode .Column ,
307+ reason : fmt .Sprintf ("must match sha256:<64 lowercase hex chars>, got %q" , digest ),
308+ },
309+ )
310+ }
311+ return ref + "@" + digest , nil
280312}
281313
282314func nonNullImageMappingScalar (n * yaml.Node ) (string , bool ) {
0 commit comments