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 }