github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/verify-install/bom-validator/bom_validator_test.go (about) 1 // Copyright (c) 2022, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package bomvalidator 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "log" 10 "os" 11 "regexp" 12 "strings" 13 "time" 14 15 . "github.com/onsi/ginkgo/v2" 16 . "github.com/onsi/gomega" 17 vzstring "github.com/verrazzano/verrazzano/pkg/string" 18 "github.com/verrazzano/verrazzano/tests/e2e/pkg" 19 "github.com/verrazzano/verrazzano/tests/e2e/pkg/test/framework" 20 corev1 "k8s.io/api/core/v1" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 ) 23 24 const ( 25 platformOperatorPodNameSearchString = "verrazzano-platform-operator" // Pod Substring for finding the platform operator pod 26 rancherWarningMessage = "Rancher shell image version may be old due to upgrade" // For known Rancher component upgrade behavior during VZ upgrade 27 shortPollingInterval = 10 * time.Second 28 shortWaitTimeout = 20 * time.Minute 29 ) 30 31 type imageDetails struct { 32 Image string `json:"image"` 33 Tag string `json:"tag"` 34 HelmFullImageKey string `json:"helmFullImageKey"` 35 } 36 37 type subComponentType struct { 38 Repository string `json:"repository"` 39 Name string `json:"name"` 40 Images []imageDetails `json:"images"` 41 } 42 43 type componentType struct { 44 Name string `json:"name"` 45 Subcomponents []subComponentType `json:"subcomponents"` 46 } 47 48 type verrazzanoBom struct { 49 Registry string `json:"registry"` 50 Version string `json:"version"` 51 Components []componentType `json:"components"` 52 } 53 54 // Capture Tags for artifact, 1 from BOM, All from images in cluster 55 type imageError struct { 56 clusterImageTag string 57 bomImageTags []string 58 } 59 60 var ( 61 kubeconfig string 62 ) 63 64 type knownIssues struct { 65 alternateTags []string 66 message string 67 } 68 69 // Rancher Helm pods hang around for 1 hour, so during an upgrade there will be a mix of old and new Rancher 70 // shell images, so exclude that image from validation 71 var knownImageIssues = map[string]knownIssues{ 72 "shell": {message: rancherWarningMessage}, 73 } 74 75 // BOM validations validates the images of below allowed namespaces only 76 var allowedNamespaces = []string{ 77 "^cattle-*", 78 "^fleet-*", 79 "^cluster-fleet-*", 80 "^cert-manager", 81 "^ingress-nginx", 82 "^istio-system", 83 "^keycloak", 84 "^monitoring", 85 "^verrazzano-*", 86 "^argocd", 87 } 88 89 var vBom verrazzanoBom // BOM from platform operator in struct form 90 var clusterImageArray []string // List of cluster installed images 91 var bomImages = make(map[string][]string) // Map of images mentioned into the BOM with associated set of tags 92 var clusterImageTagErrors = make(map[string]imageError) // Map of cluster image tags doesn't match with BOM, hence a Failure Condition 93 var clusterImagesNotFound = make(map[string]string) // Map of cluster image doesn't match with BOM, hence a Failure Condition 94 var clusterImageWarnings = make(map[string]string) // Map of image names not found in cluster. Warning/ Known Issues/ Informational. This may be valid based on profile 95 96 var t = framework.NewTestFramework("BOM validator") 97 98 var _ = BeforeSuite(beforeSuite) 99 100 var _ = t.Describe("BOM Validator", Label("f:platform-lcm.install"), func() { 101 t.Context("Post VZ Installations", func() { 102 103 t.It("Has BOM images associated with its tags", func() { 104 Expect(vBom.Components).NotTo(BeNil()) 105 }) 106 107 t.It("Has Successful BOM Validation Report", func() { 108 populateBomContainerImagesMap() 109 Expect(bomImages).NotTo(BeEmpty()) 110 Expect(scanClusterImagesWithBom()).Should(BeTrue()) 111 populateClusterContainerImages() 112 Expect(clusterImageArray).NotTo(BeEmpty()) 113 Eventually(BomValidationReport).WithPolling(shortPollingInterval).WithTimeout(shortWaitTimeout).Should(BeTrue()) 114 }) 115 }) 116 }) 117 118 var beforeSuite = t.BeforeSuiteFunc(func() { 119 Expect(validateKubeConfig()).Should(BeTrue()) 120 getBOM() 121 }) 122 123 // Validate that KubeConfig is valued. This will point to the cluster being validated 124 func validateKubeConfig() bool { 125 if kubeconfig == "" { 126 kubeconfig = os.Getenv("KUBECONFIG") 127 } 128 if kubeconfig != "" { 129 fmt.Println("USING KUBECONFIG: ", kubeconfig) 130 return true 131 } 132 return false 133 } 134 135 // Get the BOM from the platform operator in the cluster and build the BOM structure from it 136 func getBOM() { 137 var platformOperatorPodName = "" 138 pods, err := pkg.ListPods("verrazzano-install", metav1.ListOptions{}) 139 if err != nil { 140 log.Fatal(err) 141 } 142 for i := range pods.Items { 143 if strings.HasPrefix(pods.Items[i].Name, platformOperatorPodNameSearchString) { 144 platformOperatorPodName = pods.Items[i].Name 145 break 146 } 147 } 148 if platformOperatorPodName == "" { 149 log.Fatal("Platform Operator Pod Name not found in verrazzano-install namespace!") 150 } 151 152 platformOperatorPodName = strings.TrimSuffix(platformOperatorPodName, "\n") 153 fmt.Printf("The platform operator pod name is %s\n", platformOperatorPodName) 154 // Get the BOM from platform-operator 155 var command = []string{"cat", "/verrazzano/platform-operator/verrazzano-bom.json"} 156 out, _, err := pkg.Execute(platformOperatorPodName, "", "verrazzano-install", command) 157 if err != nil { 158 log.Fatal(err) 159 } 160 if len(out) == 0 { 161 log.Fatal("Error retrieving BOM from platform operator, zero length\n") 162 } 163 json.Unmarshal([]byte(out), &vBom) 164 } 165 166 // Populate BOM images into Hashmap bomImages 167 // contains a map of "image" in the BOM to validate an image found in an allowed namespace exists in the BOM 168 func populateBomContainerImagesMap() { 169 for _, component := range vBom.Components { 170 for _, subcomponent := range component.Subcomponents { 171 for _, image := range subcomponent.Images { 172 bomImages[image.Image] = append(bomImages[image.Image], image.Tag) 173 } 174 } 175 } 176 } 177 178 // Return all installed cluster namespaces 179 func getAllNamespaces() []string { 180 namespaces, err := pkg.ListNamespaces(metav1.ListOptions{}) 181 if err != nil { 182 log.Fatal(err) 183 } 184 var clusterNamespaces []string 185 for _, namespaceItem := range namespaces.Items { 186 clusterNamespaces = append(clusterNamespaces, namespaceItem.Name) 187 } 188 return clusterNamespaces 189 } 190 191 // Get the cluster namespaces and validate images of allowed namespaces only 192 // Populate an Array 'A' with all the container & initContainer images found in the cluster of allowed namespaces 193 // Send Cluster's Images Array 'A' for BOM Validations against populated BOM hashmap 'bomImages' 194 // Hashmap 'clusterImagesNotFound' are images found in allowed namespaces that are not declared in the BOM 195 // Hashmap 'clusterImageTagErrors' are images in allowed namespaces without matching tags in the BOM 196 func populateClusterImages(installedNamespace string) { 197 podsList, err := pkg.ListPods(installedNamespace, metav1.ListOptions{}) 198 if err != nil { 199 log.Fatal(err) 200 } 201 for _, pod := range podsList.Items { 202 podLabels := pod.GetLabels() 203 _, ok := podLabels["job-name"] 204 if pod.Status.Phase != corev1.PodRunning && ok { 205 continue 206 } 207 for _, initContainer := range pod.Spec.InitContainers { 208 clusterImageArray = append(clusterImageArray, initContainer.Image) 209 } 210 for _, container := range pod.Spec.Containers { 211 clusterImageArray = append(clusterImageArray, container.Image) 212 } 213 } 214 } 215 216 func populateClusterContainerImages() { 217 for _, installedNamespace := range getAllNamespaces() { 218 for _, whiteListedNamespace := range allowedNamespaces { 219 if ok, _ := regexp.MatchString(whiteListedNamespace, installedNamespace); ok { 220 populateClusterImages(installedNamespace) 221 } 222 } 223 } 224 } 225 226 // Report out the findings 227 // clusterImagesNotFound is a failure condition 228 // clusterImageTagErrors is a failure condition 229 func BomValidationReport() bool { 230 // Dump Images Not Found to Console, Informational 231 const textDivider = "----------------------------------------" 232 233 if len(clusterImageWarnings) > 0 { 234 fmt.Println() 235 fmt.Println("Image Warnings - Tags not at expected BOM level due to known issues") 236 fmt.Println(textDivider) 237 for name, msg := range clusterImageWarnings { 238 fmt.Printf("Warning: Image Name = %s: %s\n", name, msg) 239 } 240 } 241 if len(clusterImagesNotFound) > 0 { 242 fmt.Println() 243 fmt.Println("Image Errors: Images found in allowed namespaces not declared in BOM") 244 fmt.Println(textDivider) 245 for name, tag := range clusterImagesNotFound { 246 fmt.Printf("Found image in allowed namespace not declared in BOM : %s:%s\n", name, tag) 247 } 248 return false 249 } 250 if len(clusterImageTagErrors) > 0 { 251 fmt.Println() 252 fmt.Println("Image Errors: Images found in allowed namespace of cluster with unexpected tags") 253 fmt.Println(textDivider) 254 for name, tags := range clusterImageTagErrors { 255 fmt.Println("Check failed! Image Name = ", name, ", Tag from Cluster = ", tags.clusterImageTag, "Tags from BOM = ", tags.bomImageTags) 256 } 257 return false 258 } 259 fmt.Println() 260 fmt.Println("!! BOM Validation Successful !!") 261 return true 262 } 263 264 // Validate out the presence of cluster images and tags into vz BOM 265 func scanClusterImagesWithBom() bool { 266 for _, container := range clusterImageArray { 267 begin := strings.LastIndex(container, "/") 268 end := len(container) 269 containerName := container[begin+1 : end] 270 nameTag := strings.Split(containerName, ":") 271 272 // Check if the image/tag in the cluster is known to have issues 273 imageWarning, hasKnownIssues := knownImageIssues[nameTag[0]] 274 if hasKnownIssues && (imageWarning.alternateTags == nil || len(imageWarning.alternateTags) == 0 || vzstring.SliceContainsString(imageWarning.alternateTags, nameTag[1])) { 275 clusterImageWarnings[nameTag[0]] = fmt.Sprintf("Known issue for image %s, found tag %s, expected tag %s message: %s", 276 nameTag[0], nameTag[1], bomImages[nameTag[0]], imageWarning.message) 277 continue 278 } 279 // error scenarios, 280 // 1. if cluster's image not found into BOM's image map 281 if _, ok := bomImages[nameTag[0]]; !ok { 282 // cluster's image not found into BOM 283 clusterImagesNotFound[nameTag[0]] = nameTag[1] 284 continue 285 } 286 // 2. if cluster's image's version (tag) mismatched to BOM's image versions(tags) 287 if !vzstring.SliceContainsString(bomImages[nameTag[0]], nameTag[1]) { 288 // cluster's image's version (tag) mismatched to BOM image versions(tags) 289 clusterImageTagErrors[nameTag[0]] = imageError{nameTag[1], bomImages[nameTag[0]]} 290 } 291 } 292 // validation went successful 293 return true 294 }