Skip to content

Commit eee869b

Browse files
committed
HYPERFLEET-1411 - fix: Address comments
1 parent 1ec2704 commit eee869b

5 files changed

Lines changed: 201 additions & 64 deletions

File tree

.tekton/hyperfleet-operator-bundle-push.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ metadata:
1212
== "main" &&
1313
(".tekton/hyperfleet-operator-bundle-push.yaml".pathChanged() ||
1414
"bundle.Dockerfile".pathChanged() ||
15-
"config/***".pathChanged())
15+
"config/***".pathChanged() ||
16+
"validators/related-images/**".pathChanged())
1617
labels:
1718
appstudio.openshift.io/application: hyperfleet
1819
appstudio.openshift.io/component: hyperfleet-operator-bundle
@@ -228,6 +229,7 @@ spec:
228229
- $(params.build-args[*])
229230
- KUSTOMIZE_VARIANT=config/manifests/prod
230231
- BUNDLE_VERSION=0.0.1
232+
- VALIDATE_RELATED_IMAGES=true
231233
- name: BUILD_ARGS_FILE
232234
value: $(params.build-args-file)
233235
- name: PRIVILEGED_NESTED

.tekton/hyperfleet-operator-push.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ metadata:
1515
x.matches('^bundle\\.Dockerfile$')
1616
|| x.matches('^config/manifests/prod/')
1717
|| x.matches('^hack/test-disconnected-mirror\\.sh$')
18+
|| x.matches('^validators/')
1819
|| x.matches('^\\.tekton/hyperfleet-operator-bundle-push\\.yaml$')
1920
|| x.matches('^catalog/')
2021
|| x.matches('^catalog\\.Dockerfile$')

bundle.Dockerfile

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,24 @@ FROM quay.io/konflux-ci/operator-sdk-builder:latest@sha256:bd34ca58b2d08e8ee3b9c
1111

1212
WORKDIR /workdir
1313
COPY config/ ./config/
14-
1514
COPY --from=validator /workdir/bin/related-images-validator ./related-images-validator
16-
# Specify the kustomize variant, either bases/kustomization.yaml or prod/kustomization.yaml
17-
# prod/kustomization.yaml gets image update references from konflux.
15+
16+
# Specify the kustomize variant, either config/manifests/dev or config/manifests/prod
1817
ARG KUSTOMIZE_VARIANT=config/manifests/dev
19-
# ARG KUSTOMIZE_VARIANT=config/manifests/prod for konflux builds
2018
RUN kustomize build /workdir/${KUSTOMIZE_VARIANT} > /workdir/manifests.yaml
2119

2220
ARG CHANNELS=stable
2321
ARG DEFAULT_CHANNEL=stable
2422
ARG BUNDLE_VERSION=0.0.1
25-
2623
RUN mkdir -p /workdir/bundle
2724
RUN cat manifests.yaml | operator-sdk generate bundle -q --version ${BUNDLE_VERSION} \
2825
--channels=${CHANNELS} --default-channel=${DEFAULT_CHANNEL} \
2926
--package=hyperfleet-operator && \
30-
operator-sdk bundle validate ./bundle --select-optional name=operatorhubv2 && \
31-
if [ "${VALIDATE_RELATED_IMAGES}" = "true" ]; then \
32-
operator-sdk bundle validate ./bundle --alpha-select-external ./related-images-validator; \
27+
operator-sdk bundle validate ./bundle --select-optional name=operatorhubv2
28+
29+
ARG VALIDATE_RELATED_IMAGES=true
30+
RUN if [ "$VALIDATE_RELATED_IMAGES" = "true" ]; then \
31+
./related-images-validator -csv bundle/manifests/*.clusterserviceversion.yaml; \
3332
fi
3433

3534
FROM scratch

validators/related-images/main.go

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
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.
114
package main
215

316
import (
@@ -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

99119
func 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

205229
func 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

232256
func errMsg(field, detail string) validationMsg {

0 commit comments

Comments
 (0)