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  }