1+ // Copyright 2026.
2+ //
3+ // Licensed under the Apache License, Version 2.0 (the "License");
4+ // you may not use this file except in compliance with the License.
5+ // You may obtain a copy of the License at
6+ //
7+ // http://www.apache.org/licenses/LICENSE-2.0
8+ //
9+ // Unless required by applicable law or agreed to in writing, software
10+ // distributed under the License is distributed on an "AS IS" BASIS,
11+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ // See the License for the specific language governing permissions and
13+ // limitations under the License.
114package main
215
316import (
@@ -52,7 +65,8 @@ type deployment struct {
5265 Spec struct {
5366 Template struct {
5467 Spec struct {
55- Containers []container `json:"containers"`
68+ Containers []container `json:"containers"`
69+ InitContainers []container `json:"initContainers"`
5670 } `json:"spec"`
5771 } `json:"template"`
5872 } `json:"spec"`
@@ -94,14 +108,20 @@ func main() {
94108 os .Exit (1 )
95109 }
96110 fmt .Println (string (out ))
111+
112+ // operator-sdk reads errors from JSON output, so exit 0 is fine there.
113+ // Standalone -csv mode needs a non-zero exit code for CI.
114+ if * csvPath != "" && len (result .Errors ) > 0 {
115+ os .Exit (1 )
116+ }
97117}
98118
99119func validate (bundleRoot string ) manifestResult {
100- csvPath := findCSV (bundleRoot )
101- if csvPath == "" {
120+ csvPath , err := findCSV (bundleRoot )
121+ if err != nil {
102122 return manifestResult {
103123 Name : validatorName ,
104- Errors : []validationMsg {errMsg ("" , "no ClusterServiceVersion found in bundle manifests" )},
124+ Errors : []validationMsg {errMsg ("" , fmt . Sprintf ( "failed to find CSV: %v" , err ) )},
105125 }
106126 }
107127
@@ -125,81 +145,85 @@ func validateCSVData(data []byte) manifestResult {
125145 return result
126146 }
127147
148+ containerImages := validateContainerImages (& result , csv .Spec .Install .Spec .Deployments )
149+ relatedImages := validateRelatedImages (& result , csv .Spec .RelatedImages , containerImages )
150+ validateEnvVars (& result , csv .Spec .Install .Spec .Deployments , relatedImages )
151+
152+ for image , matched := range containerImages {
153+ if ! matched {
154+ result .Errors = append (result .Errors , errMsg ("spec.relatedImages" ,
155+ fmt .Sprintf ("container image not in relatedImages: %q" , image )))
156+ }
157+ }
158+ return result
159+ }
160+
161+ // validateContainerImages checks that each container image is a sha256 digest pullspec.
162+ func validateContainerImages (result * manifestResult , deployments []deployment ) map [string ]bool {
128163 containerImages := make (map [string ]bool )
129- for _ , dep := range csv . Spec . Install . Spec . Deployments {
130- for _ , c := range dep .Spec .Template .Spec .Containers {
131- // Add these to a managerImages map
164+ for _ , dep := range deployments {
165+ allContainers := append ( dep .Spec .Template .Spec .Containers , dep . Spec . Template . Spec . InitContainers ... )
166+ for _ , c := range allContainers {
132167 containerImages [c .Image ] = false
133168 if ! isSHA256DigestPullspec (c .Image ) {
134169 result .Errors = append (result .Errors , errMsg ("relatedImages" ,
135- fmt .Sprintf ("not using sha256 in image tag : %v" , c .Image )))
170+ fmt .Sprintf ("image reference is not a sha256 digest : %v" , c .Image )))
136171 }
137172 }
138173 }
174+ return containerImages
175+ }
139176
177+ // validateRelatedImages checks for duplicate entries and non-sha256 digests in spec.relatedImages.
178+ func validateRelatedImages (result * manifestResult , images []imageEntry , containerImages map [string ]bool ) map [string ]string {
140179 relatedImages := make (map [string ]string )
141- for _ , image := range csv . Spec . RelatedImages {
180+ for _ , image := range images {
142181 if ! isSHA256DigestPullspec (image .Image ) {
143182 result .Errors = append (result .Errors , errMsg ("relatedImages" ,
144- fmt .Sprintf ("not using sha256 in image tag : %v" , image .Image )))
183+ fmt .Sprintf ("image reference is not a sha256 digest : %v" , image .Image )))
145184 }
146- _ , ok := relatedImages [image .Image ]
147- if ! ok {
148- // no match in the map, add them and continue
185+ if _ , ok := relatedImages [image .Image ]; ! ok {
149186 relatedImages [image .Image ] = image .Name
150-
151- // check if relatedImage is a container image, if so, mark it as added.
152187 if _ , ok := containerImages [image .Image ]; ok {
153188 containerImages [image .Image ] = true
154189 }
155190 } else {
156- // duplicate in map, add to result.Errors
157191 result .Errors = append (result .Errors , errMsg ("relatedImages" ,
158192 fmt .Sprintf ("duplicate value of image: %v" , image .Image )))
159193 }
160194 }
195+ return relatedImages
196+ }
161197
162- // duplicate env var
163- envRelatedImages := []string {}
164- for _ , dep := range csv .Spec .Install .Spec .Deployments {
165- for _ , c := range dep .Spec .Template .Spec .Containers {
198+ // validateEnvVars checks that RELATED_IMAGE_ env vars use sha256 digests, are not duplicated
199+ // within a container, and have a corresponding entry in spec.relatedImages.
200+ func validateEnvVars (result * manifestResult , deployments []deployment , relatedImages map [string ]string ) {
201+ for _ , dep := range deployments {
202+ allContainers := append (dep .Spec .Template .Spec .Containers , dep .Spec .Template .Spec .InitContainers ... )
203+ for _ , c := range allContainers {
204+ envRelatedImages := []string {}
166205 for _ , e := range c .Env {
167- if strings .HasPrefix (e .Name , "RELATED_IMAGE_" ) {
206+ if strings .HasPrefix (e .Name , relatedImagePrefix ) {
168207 if ! isSHA256DigestPullspec (e .Value ) {
169208 result .Errors = append (result .Errors , errMsg ("relatedImages" ,
170- fmt .Sprintf ("not using sha256 in image tag : %v" , e .Value )))
209+ fmt .Sprintf ("image reference is not a sha256 digest : %v" , e .Value )))
171210 }
172- // verify that every value is in relatedImages section
173- if _ , ok := relatedImages [e .Value ]; ! ok {
211+ if slices .Contains (envRelatedImages , e .Name ) {
174212 result .Errors = append (result .Errors ,
175213 errMsg ("relatedImages" ,
176- fmt .Sprintf ("env var not add to relatedImages: %v " , e )))
214+ fmt .Sprintf ("duplicate env var %s in container %s " , e . Name , c . Name )))
177215 continue
178- } else {
179- if slices .Contains (envRelatedImages , e .Name ) {
180- // duplicate found
181- result .Errors = append (result .Errors ,
182- errMsg ("relatedImages" ,
183- fmt .Sprintf ("env var duplicated: %v" , e )))
184- continue
185- } else {
186- envRelatedImages = append (envRelatedImages , e .Name )
187- }
216+ }
217+ envRelatedImages = append (envRelatedImages , e .Name )
218+ if _ , ok := relatedImages [e .Value ]; ! ok {
219+ result .Errors = append (result .Errors ,
220+ errMsg ("relatedImages" ,
221+ fmt .Sprintf ("env var %s not found in spec.relatedImages (value: %s)" , e .Name , e .Value )))
188222 }
189223 }
190224 }
191225 }
192226 }
193-
194- // Verify that containerImages all include true
195- for image , matched := range containerImages {
196- if ! matched {
197- result .Errors = append (result .Errors , errMsg ("spec.relatedImages" ,
198- fmt .Sprintf ("container image not in relatedImages:%q" , image )))
199- }
200- }
201- // check that images are all sha digests
202- return result
203227}
204228
205229func isSHA256DigestPullspec (image string ) bool {
@@ -215,18 +239,18 @@ func isSHA256DigestPullspec(image string) bool {
215239 return digest .Algorithm ().String () == "sha256" && digest .Validate () == nil
216240}
217241
218- func findCSV (bundleRoot string ) string {
242+ func findCSV (bundleRoot string ) ( string , error ) {
219243 manifestsDir := bundleRoot + "/manifests"
220244 entries , err := os .ReadDir (manifestsDir )
221245 if err != nil {
222- return ""
246+ return "" , fmt . Errorf ( "reading manifests directory: %w" , err )
223247 }
224248 for _ , e := range entries {
225249 if strings .HasSuffix (e .Name (), ".clusterserviceversion.yaml" ) {
226- return manifestsDir + "/" + e .Name ()
250+ return manifestsDir + "/" + e .Name (), nil
227251 }
228252 }
229- return ""
253+ return "" , fmt . Errorf ( "no CSV found in manifests directory" )
230254}
231255
232256func errMsg (field , detail string ) validationMsg {
0 commit comments