github.com/verrazzano/verrazzano@v1.7.1/tools/vz/pkg/internal/util/cluster/certificates.go (about)

     1  // Copyright (c) 2023, 2024, 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 cluster handles cluster analysis
     5  package cluster
     6  
     7  import (
     8  	encjson "encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"math"
    12  	"os"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	certv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
    18  	"github.com/verrazzano/verrazzano/tools/vz/pkg/constants"
    19  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    20  	"github.com/verrazzano/verrazzano/tools/vz/pkg/internal/util/files"
    21  	"github.com/verrazzano/verrazzano/tools/vz/pkg/internal/util/report"
    22  	"go.uber.org/zap"
    23  )
    24  
    25  // AnalyzeCertificateRelatedIssues is the initial entry function for certificate related issues and it returns an error.
    26  // It first determines the status of the VZ Client, then checks if there are any certificates in the namespaces.
    27  // It then analyzes those certificates to determine expiration or other issues and then contributes the respective issues to the Issue Reporter.
    28  // The three issues that it is currently reporting on are the VZ Client hanging due to a long time to issues validate certificates, expired certificates, and when the certificate is not in a ready status.
    29  func AnalyzeCertificateRelatedIssues(log *zap.SugaredLogger, clusterRoot string) (err error) {
    30  	mapOfCertificatesInVPOToTheirNamespace, err := determineIfVZClientIsHangingDueToCerts(log, clusterRoot)
    31  
    32  	if err != nil {
    33  		return err
    34  	}
    35  	allNamespacesFound, err = files.FindNamespaces(log, clusterRoot)
    36  	if err != nil {
    37  		return err
    38  	}
    39  	var issueReporter = report.IssueReporter{
    40  		PendingIssues: make(map[string]report.Issue),
    41  	}
    42  	for _, namespace := range allNamespacesFound {
    43  		certificateFile := files.FormFilePathInNamespace(clusterRoot, namespace, constants.CertificatesJSON)
    44  		certificateListForNamespace, err := getCertificateList(log, certificateFile)
    45  		if err != nil {
    46  			return err
    47  		}
    48  		if certificateListForNamespace == nil {
    49  			continue
    50  		}
    51  
    52  		for _, certificate := range certificateListForNamespace.Items {
    53  			if getLatestCondition(log, certificate) == nil {
    54  				continue
    55  			}
    56  			conditionOfCert := getLatestCondition(log, certificate)
    57  			if isCertConditionValid(conditionOfCert) && isVZClientHangingOnCert(mapOfCertificatesInVPOToTheirNamespace, certificate) {
    58  				reportVZClientHangingIssue(log, clusterRoot, certificate, &issueReporter, certificateFile)
    59  				continue
    60  			}
    61  			if !(isCertConditionValid(conditionOfCert)) {
    62  				reportGenericCertificateIssue(log, clusterRoot, certificate, &issueReporter, certificateFile)
    63  				continue
    64  			}
    65  			if certificate.Status.NotAfter.Unix() < time.Now().Unix() {
    66  				reportCertificateExpirationIssue(log, clusterRoot, certificate, &issueReporter, certificateFile)
    67  			}
    68  
    69  		}
    70  		caCrtFile := files.FormFilePathInNamespace(clusterRoot, namespace, "caCrtInfo.json")
    71  		caCrtListForNamespace, err := getCaCertInfoFromFile(log, caCrtFile)
    72  		if err != nil {
    73  			return err
    74  		}
    75  		if caCrtListForNamespace == nil {
    76  			continue
    77  		}
    78  		for _, caCrtInfo := range *caCrtListForNamespace {
    79  			if caCrtInfo.Expired {
    80  				reportCaCrtExpirationIssue(log, clusterRoot, caCrtInfo, &issueReporter, caCrtFile, namespace)
    81  			}
    82  		}
    83  
    84  	}
    85  	issueReporter.Contribute(log, clusterRoot)
    86  	return nil
    87  
    88  }
    89  
    90  // isCertConditionValid returns a boolean value that is true if a condition of a certificate is valid and false otherwise
    91  func isCertConditionValid(conditionOfCert *certv1.CertificateCondition) bool {
    92  	return conditionOfCert.Status == "True" && conditionOfCert.Type == "Ready" && conditionOfCert.Message == "Certificate is up to date and has not expired"
    93  }
    94  
    95  // isVZClientHangingOnCertDetermines returns a boolean value that is true if the VZ Client is currently hanging on a certificate and false otherwise
    96  func isVZClientHangingOnCert(mapOfCertsThatVZClientIsHangingOn map[string]string, certificate certv1.Certificate) bool {
    97  	if len(mapOfCertsThatVZClientIsHangingOn) <= 0 {
    98  		return false
    99  	}
   100  	namespace, ok := mapOfCertsThatVZClientIsHangingOn[certificate.ObjectMeta.Name]
   101  	if ok && namespace == certificate.ObjectMeta.Namespace {
   102  		return true
   103  	}
   104  	return false
   105  }
   106  
   107  // getCertificateList returns a list of certificate objects based on the certificates.json file
   108  func getCertificateList(log *zap.SugaredLogger, path string) (certificateList *certv1.CertificateList, err error) {
   109  	certList := &certv1.CertificateList{}
   110  	file, err := os.Open(path)
   111  	if err != nil {
   112  		log.Debug("file %s not found", path)
   113  		return nil, nil
   114  	}
   115  	defer file.Close()
   116  	fileBytes, err := io.ReadAll(file)
   117  	if err != nil {
   118  		log.Error("Failed reading Certificates.json file %s", path)
   119  		return nil, err
   120  	}
   121  	err = encjson.Unmarshal(fileBytes, &certList)
   122  	if err != nil {
   123  		log.Error("Failed to unmarshal CertificateList at %s", path)
   124  		return nil, err
   125  	}
   126  	return certList, err
   127  }
   128  func getCaCertInfoFromFile(log *zap.SugaredLogger, path string) (caCrtInfo *[]helpers.CaCrtInfo, err error) {
   129  	caCrtList := &[]helpers.CaCrtInfo{}
   130  	file, err := os.Open(path)
   131  	if err != nil {
   132  		log.Debug("file %s not found", path)
   133  		return nil, nil
   134  	}
   135  	defer file.Close()
   136  	fileBytes, err := io.ReadAll(file)
   137  	if err != nil {
   138  		log.Error("Failed reading Certificates.json file %s", path)
   139  		return nil, err
   140  	}
   141  	err = encjson.Unmarshal(fileBytes, &caCrtList)
   142  	if err != nil {
   143  		log.Error("Failed to unmarshal CertificateList at %s", path)
   144  		return nil, err
   145  	}
   146  	return caCrtList, err
   147  }
   148  
   149  // getLatestCondition returns the latest condition in a certificate, if one exists
   150  func getLatestCondition(log *zap.SugaredLogger, certificate certv1.Certificate) *certv1.CertificateCondition {
   151  	if certificate.Status.Conditions == nil {
   152  		return nil
   153  	}
   154  	var latestCondition *certv1.CertificateCondition
   155  	latestCondition = nil
   156  	conditions := certificate.Status.Conditions
   157  	for i, condition := range conditions {
   158  		if condition.LastTransitionTime == nil {
   159  			continue
   160  		}
   161  		if latestCondition == nil && condition.LastTransitionTime != nil {
   162  			latestCondition = &(conditions[i])
   163  			continue
   164  		}
   165  		if latestCondition.LastTransitionTime.UnixNano() < condition.LastTransitionTime.UnixNano() {
   166  			latestCondition = &(conditions[i])
   167  		}
   168  
   169  	}
   170  	return latestCondition
   171  }
   172  
   173  // reportVZClientHangingIssue reports when a VZ Client issue has occurred due to certificate approval
   174  func reportVZClientHangingIssue(log *zap.SugaredLogger, clusterRoot string, certificate certv1.Certificate, issueReporter *report.IssueReporter, certificateFile string) {
   175  	files := []string{certificateFile}
   176  	message := []string{fmt.Sprintf("The VZ Client is hanging due to a long time for the certificate to complete, but the certificate named %s in namespace %s is ready", certificate.ObjectMeta.Name, certificate.ObjectMeta.Namespace)}
   177  	issueReporter.AddKnownIssueMessagesFiles(report.VZClientHangingIssueDueToLongCertificateApproval, clusterRoot, message, files)
   178  
   179  }
   180  
   181  // reportCertificateExpirationIssue reports if a certificate has expired
   182  func reportCertificateExpirationIssue(log *zap.SugaredLogger, clusterRoot string, certificate certv1.Certificate, issueReporter *report.IssueReporter, certificateFile string) {
   183  	files := []string{certificateFile}
   184  	message := []string{fmt.Sprintf("The certificate named %s in namespace %s is expired", certificate.ObjectMeta.Name, certificate.ObjectMeta.Namespace)}
   185  	issueReporter.AddKnownIssueMessagesFiles(report.CertificateExpired, clusterRoot, message, files)
   186  }
   187  
   188  // This function reports when a certificate is not expired, and the VPO is not hanging, but an issue has occurred.
   189  func reportGenericCertificateIssue(log *zap.SugaredLogger, clusterRoot string, certificate certv1.Certificate, issueReporter *report.IssueReporter, certificateFile string) {
   190  	files := []string{certificateFile}
   191  	message := []string{fmt.Sprintf("The certificate named %s in namespace %s is not valid and experiencing issues", certificate.ObjectMeta.Name, certificate.ObjectMeta.Namespace)}
   192  	issueReporter.AddKnownIssueMessagesFiles(report.CertificateExperiencingIssuesInCluster, clusterRoot, message, files)
   193  }
   194  func reportCaCrtExpirationIssue(log *zap.SugaredLogger, clusterRoot string, caCrtInfoEntry helpers.CaCrtInfo, issueReporter *report.IssueReporter, caCertInfoFile string, namespace string) {
   195  	files := []string{caCertInfoFile}
   196  	message := []string{fmt.Sprintf("The ca.crt that is in secret %s in namespace %s is expired", caCrtInfoEntry.Name, namespace)}
   197  	issueReporter.AddKnownIssueMessagesFiles(report.CaCrtExpiredInCluster, clusterRoot, message, files)
   198  }
   199  
   200  // determineIfVZClientIsHangingDueToCerts determines if the VZ client is currently hanging due to certificate issues
   201  // It does this by checking the last 10 logs of the VPO and determines all the certificates that the VZ Client is hanging on
   202  // It returns a map containing these certificates as keys and their respective namespaces as values, along with an error
   203  // This map is used by the main certificate analysis function to determine if the VZ Client is hanging on a valid certificate
   204  func determineIfVZClientIsHangingDueToCerts(log *zap.SugaredLogger, clusterRoot string) (map[string]string, error) {
   205  	listOfCertificatesThatVZClientIsHangingOn := make(map[string]string)
   206  	vpologRegExp := regexp.MustCompile(`verrazzano-install/verrazzano-platform-operator-.*/logs.txt`)
   207  	allPodFiles, err := files.GetMatchingFileNames(log, clusterRoot, vpologRegExp)
   208  	if err != nil {
   209  		return listOfCertificatesThatVZClientIsHangingOn, err
   210  	}
   211  	if len(allPodFiles) == 0 {
   212  		return listOfCertificatesThatVZClientIsHangingOn, nil
   213  	}
   214  	vpoLog := allPodFiles[0]
   215  	allMessages, err := files.ConvertToLogMessage(vpoLog)
   216  	if err != nil {
   217  		log.Error("Failed to convert files to the vpo message")
   218  		return listOfCertificatesThatVZClientIsHangingOn, err
   219  	}
   220  	//If the VPO has greater than 10 messages, the last 10 logs are the input. Else, the whole VPO logs are the input
   221  	lastTenVPOLogs := allMessages[int(math.Max(float64(0), float64(len(allMessages)-10))):]
   222  	//If the VPO has greater than 10 messages, the last 10 logs are the input. Else, the whole VPO logs are the input
   223  	for _, VPOLog := range lastTenVPOLogs {
   224  		VPOLogMessage := VPOLog.Message
   225  		if strings.Contains(VPOLogMessage, "message: Issuing certificate as Secret does not exist") && strings.HasPrefix(VPOLogMessage, "Certificate ") {
   226  			VPOLogCertificateNameAndNamespace := strings.Split(VPOLogMessage, " ")[1]
   227  			namespaceAndCertificateNameSplit := strings.Split(VPOLogCertificateNameAndNamespace, "/")
   228  			nameSpace := namespaceAndCertificateNameSplit[0]
   229  			certificateName := namespaceAndCertificateNameSplit[1]
   230  			_, ok := listOfCertificatesThatVZClientIsHangingOn[certificateName]
   231  			if !ok {
   232  				listOfCertificatesThatVZClientIsHangingOn[certificateName] = nameSpace
   233  			}
   234  		}
   235  
   236  	}
   237  	return listOfCertificatesThatVZClientIsHangingOn, nil
   238  }