From 0c5b1ebc7646eed3c91260ca21424999c0906af8 Mon Sep 17 00:00:00 2001 From: Anderson Nogueira Date: Tue, 26 Aug 2025 08:31:14 +0200 Subject: [PATCH 1/2] Implement automatic SSL certificate configuration for OpenShift clusters - Add Let's Encrypt support via cert-manager for OpenShift routes - Add AWS ACM certificate management for LoadBalancer services - Implement DNS automation with Route53 - Auto-switch PMM to LoadBalancer when SSL is enabled - Add ACM certificate creation with DNS validation Changes: - New vars/openshiftSSL.groovy library for Let's Encrypt management - New vars/awsCertificates.groovy library for ACM and Route53 - Enhanced openshift_cluster_create pipeline with SSL parameters - Modified deployPMM() to support ACM certificates automatically Related to: PMM-14242 --- cloud/jenkins/openshift-cluster-create.yml | 31 + cloud/jenkins/openshift_cluster_create.groovy | 117 +++ vars/awsCertificates.groovy | 718 ++++++++++++++++++ vars/openshiftCluster.groovy | 97 ++- vars/openshiftSSL.groovy | 568 ++++++++++++++ 5 files changed, 1527 insertions(+), 4 deletions(-) create mode 100644 vars/awsCertificates.groovy create mode 100644 vars/openshiftSSL.groovy diff --git a/cloud/jenkins/openshift-cluster-create.yml b/cloud/jenkins/openshift-cluster-create.yml index 0ba7d33b99..d2d196da55 100644 --- a/cloud/jenkins/openshift-cluster-create.yml +++ b/cloud/jenkins/openshift-cluster-create.yml @@ -145,6 +145,37 @@ description: "Product/project tag for billing allocation" trim: true + # === SSL Certificate Configuration === + - bool: + name: ENABLE_SSL + default: false + description: "Enable automatic SSL certificate configuration for cluster services" + - choice: + name: SSL_METHOD + choices: + - "acm" + - "letsencrypt" + description: "SSL certificate provider (AWS ACM or Let's Encrypt via cert-manager)" + - string: + name: SSL_EMAIL + default: "admin@percona.com" + description: "Email address for Let's Encrypt registration (required for letsencrypt method)" + trim: true + - bool: + name: USE_STAGING_CERT + default: false + description: "Use Let's Encrypt staging certificates for testing (avoids rate limits)" + - string: + name: CONSOLE_CUSTOM_DOMAIN + default: "" + description: "Custom domain for OpenShift console (optional, auto-generates if empty)" + trim: true + - string: + name: PMM_CUSTOM_DOMAIN + default: "" + description: "Custom domain for PMM interface (optional, auto-generates if empty)" + trim: true + # === Advanced Options === - string: name: BASE_DOMAIN diff --git a/cloud/jenkins/openshift_cluster_create.groovy b/cloud/jenkins/openshift_cluster_create.groovy index 4e9bd650b9..bc9e9bd782 100644 --- a/cloud/jenkins/openshift_cluster_create.groovy +++ b/cloud/jenkins/openshift_cluster_create.groovy @@ -372,6 +372,13 @@ Starting cluster creation process... pmmHelmChartVersion: params.PMM_HELM_CHART_VERSION, pmmImageRepository: params.PMM_IMAGE_REPOSITORY, pmmAdminPassword: params.PMM_ADMIN_PASSWORD ?: '', // Default to auto-generation + // SSL Configuration + enableSSL: params.ENABLE_SSL, + sslMethod: params.SSL_METHOD, + sslEmail: params.SSL_EMAIL, + useStaging: params.USE_STAGING_CERT, + consoleCustomDomain: params.CONSOLE_CUSTOM_DOMAIN, + pmmCustomDomain: params.PMM_CUSTOM_DOMAIN, buildUser: env.BUILD_USER_ID ?: 'jenkins', accessKey: AWS_ACCESS_KEY_ID, secretKey: AWS_SECRET_ACCESS_KEY @@ -410,6 +417,103 @@ Starting cluster creation process... } } + stage('Configure SSL Certificates') { + when { + expression { params.ENABLE_SSL && env.CLUSTER_DIR } + } + steps { + script { + echo "" + echo "=====================================================================" + echo "Configuring SSL Certificates" + echo "=====================================================================" + echo "" + echo "SSL Method: ${params.SSL_METHOD}" + echo "Base Domain: ${params.BASE_DOMAIN}" + + def sslConfig = [ + clusterName: env.FINAL_CLUSTER_NAME, + baseDomain: params.BASE_DOMAIN, + kubeconfig: env.KUBECONFIG, + method: params.SSL_METHOD, + email: params.SSL_EMAIL, + useStaging: params.USE_STAGING_CERT + ] + + def sslResults = [:] + + if (params.SSL_METHOD == 'letsencrypt') { + echo "Setting up Let's Encrypt certificates..." + + // Configure console domain + def consoleDomain = params.CONSOLE_CUSTOM_DOMAIN ?: + "console-${env.FINAL_CLUSTER_NAME}.${params.BASE_DOMAIN}" + + sslConfig.consoleDomain = consoleDomain + + // Setup Let's Encrypt + sslResults = openshiftSSL.setupLetsEncrypt(sslConfig) + + if (sslResults.consoleCert) { + echo "✓ Console certificate configured for: ${consoleDomain}" + env.CONSOLE_SSL_DOMAIN = consoleDomain + } + } else if (params.SSL_METHOD == 'acm') { + echo "Setting up AWS ACM certificates..." + + withCredentials([ + aws( + credentialsId: 'jenkins-openshift-aws', + accessKeyVariable: 'AWS_ACCESS_KEY_ID', + secretKeyVariable: 'AWS_SECRET_ACCESS_KEY' + ) + ]) { + def services = [] + + // Add PMM service if deployed + if (params.DEPLOY_PMM && env.PMM_URL) { + def pmmDomain = params.PMM_CUSTOM_DOMAIN ?: + "pmm-${env.FINAL_CLUSTER_NAME}.${params.BASE_DOMAIN}" + + services.add([ + name: 'monitoring-service', + namespace: 'pmm-monitoring', + domain: pmmDomain + ]) + } + + sslConfig.services = services + sslConfig.accessKey = AWS_ACCESS_KEY_ID + sslConfig.secretKey = AWS_SECRET_ACCESS_KEY + + sslResults = awsCertificates.setupACM(sslConfig) + + if (sslResults.services) { + sslResults.services.each { name, config -> + if (config.configured) { + echo "✓ Service ${name} configured with ACM certificate" + if (config.domain) { + echo " Domain: https://${config.domain}" + } + } + } + } + } + } + + // Store SSL results for post-creation display + env.SSL_CONFIGURED = sslResults ? 'true' : 'false' + + if (sslResults.errors && !sslResults.errors.isEmpty()) { + echo "SSL configuration completed with warnings:" + sslResults.errors.each { error -> + echo " ⚠ ${error}" + } + } + } + } + } + stage('Post-Creation Tasks') { steps { script { @@ -435,6 +539,12 @@ Starting cluster creation process... echo "------------------" echo "API URL: ${env.CLUSTER_API_URL}" echo "Console URL: ${env.CLUSTER_CONSOLE_URL ?: 'Pending...'}" + + // Display SSL console URL if configured + if (params.ENABLE_SSL && params.SSL_METHOD == 'letsencrypt' && env.CONSOLE_SSL_DOMAIN) { + echo "Console SSL URL: https://${env.CONSOLE_SSL_DOMAIN}" + } + echo "Kubeconfig: Available in Jenkins artifacts" echo "" @@ -460,6 +570,13 @@ Starting cluster creation process... echo "Access URL: ${env.PMM_URL}" echo "Username: admin" echo "Password: ${passwordInfo}" + + // Display SSL info if configured + if (params.ENABLE_SSL && params.SSL_METHOD == 'acm' && env.SSL_CONFIGURED == 'true') { + def pmmDomain = params.PMM_CUSTOM_DOMAIN ?: + "pmm-${env.FINAL_CLUSTER_NAME}.${params.BASE_DOMAIN}" + echo "SSL Access: https://${pmmDomain}" + } echo "" } else if (params.DEPLOY_PMM) { echo "PMM DEPLOYMENT STATUS: NOT DEPLOYED" diff --git a/vars/awsCertificates.groovy b/vars/awsCertificates.groovy new file mode 100644 index 0000000000..db3fddae54 --- /dev/null +++ b/vars/awsCertificates.groovy @@ -0,0 +1,718 @@ +/** + * AWS Certificate Manager and Route53 integration library for SSL automation. + * + * This library provides AWS ACM certificate management and Route53 DNS automation + * for OpenShift clusters and services running on AWS infrastructure. + * + * @since 1.0.0 + */ + +/** + * Finds an existing ACM certificate matching the specified domain. + * + * Searches for valid (ISSUED) certificates that match the exact domain + * or a wildcard certificate that covers the domain. + * + * @param config Map containing: + * - domain: Domain name to match (required) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * - includeWildcard: Also search for wildcard certificates (optional, default: true) + * + * @return Certificate ARN if found, null otherwise + */ +def findACMCertificate(Map config = [:]) { + def params = [ + region: 'us-east-2', + profile: 'percona-dev-admin', + includeWildcard: true + ] + config + + if (!params.domain) { + error 'Missing required parameter: domain' + } + + openshiftTools.log('INFO', "Searching for ACM certificate for domain: ${params.domain}") + + try { + // Build query for wildcard domain if applicable + def wildcardDomain = '' + if (params.includeWildcard && params.domain.contains('.')) { + wildcardDomain = '*.${params.domain.substring(params.domain.indexOf('.') + 1)}' + } + + // Search for certificates + def query = wildcardDomain ? + "CertificateSummaryList[?Status=='ISSUED' && (DomainName=='${params.domain}' || DomainName=='${wildcardDomain}')].CertificateArn" : + "CertificateSummaryList[?Status=='ISSUED' && DomainName=='${params.domain}'].CertificateArn" + + def certificateArn = sh( + script: """ + aws acm list-certificates \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --query "${query}" \\ + --output text 2>/dev/null | head -1 + """, + returnStdout: true + ).trim() + + if (certificateArn && certificateArn != 'None') { + openshiftTools.log('INFO', "Found ACM certificate: ${certificateArn}") + + // Verify certificate details + def certDetails = sh( + script: """ + aws acm describe-certificate \\ + --certificate-arn ${certificateArn} \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --query 'Certificate.{Status:Status,DomainName:DomainName,SubjectAlternativeNames:SubjectAlternativeNames}' \\ + --output json + """, + returnStdout: true + ).trim() + + openshiftTools.log('DEBUG', "Certificate details: ${certDetails}") + return certificateArn + } else { + openshiftTools.log('WARN', "No ACM certificate found for domain: ${params.domain}") + return null + } + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to find ACM certificate: ${e.message}") + return null + } +} + +/** + * Applies an ACM certificate to a Kubernetes LoadBalancer service. + * + * Configures the service annotations to use the specified ACM certificate + * for SSL/TLS termination at the AWS Load Balancer level. + * + * @param config Map containing: + * - namespace: Kubernetes namespace (required) + * - serviceName: Service name (required) + * - certificateArn: ACM certificate ARN (required) + * - kubeconfig: Path to kubeconfig file (required) + * - backendProtocol: Backend protocol (optional, default: 'https') + * - sslPorts: SSL ports (optional, default: '443') + * + * @return Boolean indicating success + */ +def applyACMToLoadBalancer(Map config) { + def params = [ + backendProtocol: 'https', + sslPorts: '443' + ] + config + + def required = ['namespace', 'serviceName', 'certificateArn', 'kubeconfig'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + env.KUBECONFIG = params.kubeconfig + openshiftTools.log('INFO', "Applying ACM certificate to service ${params.serviceName} in namespace ${params.namespace}") + + try { + // Create patch file with ACM annotations + def patchFile = "/tmp/acm-patch-${BUILD_NUMBER}.yaml" + writeFile file: patchFile, text: """ +metadata: + annotations: + service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "${params.backendProtocol}" + service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "${params.certificateArn}" + service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "${params.sslPorts}" +""" + + // Apply patch to service + sh """ + oc patch service ${params.serviceName} \\ + -n ${params.namespace} \\ + --patch-file ${patchFile} + """ + + // Cleanup patch file + sh "rm -f ${patchFile}" + + // Wait for LoadBalancer to update + openshiftTools.log('INFO', 'Waiting for LoadBalancer to update with certificate...') + sleep(time: 30, unit: 'SECONDS') + + // Verify LoadBalancer has the certificate annotation + def annotations = sh( + script: """ + oc get service ${params.serviceName} -n ${params.namespace} \\ + -o jsonpath='{.metadata.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-ssl-cert}' + """, + returnStdout: true + ).trim() + + if (annotations == params.certificateArn) { + openshiftTools.log('INFO', 'ACM certificate applied successfully') + return true + } else { + openshiftTools.log('WARN', 'Certificate annotation not found after patch') + return false + } + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to apply ACM certificate: ${e.message}") + return false + } +} + +/** + * Creates or updates a Route53 DNS record. + * + * @param config Map containing: + * - domain: Domain name for the record (required) + * - value: Record value (CNAME target or A record IP) (required) + * - hostedZoneId: Route53 hosted zone ID (optional, will auto-detect if not provided) + * - recordType: DNS record type (optional, default: 'CNAME') + * - ttl: Time to live in seconds (optional, default: 300) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * - action: Change action (optional, default: 'UPSERT') + * + * @return Boolean indicating success + */ +def createRoute53Record(Map config) { + def params = [ + recordType: 'CNAME', + ttl: 300, + region: 'us-east-2', + profile: 'percona-dev-admin', + action: 'UPSERT' + ] + config + + def required = ['domain', 'value'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + openshiftTools.log('INFO', "Creating Route53 ${params.recordType} record for ${params.domain}") + + try { + // Auto-detect hosted zone if not provided + if (!params.hostedZoneId) { + params.hostedZoneId = getHostedZoneId([ + domain: params.domain, + region: params.region, + profile: params.profile + ]) + + if (!params.hostedZoneId) { + error "Could not find hosted zone for domain ${params.domain}" + } + } + + // Create change batch JSON + def changeBatch = """ +{ + "Changes": [{ + "Action": "${params.action}", + "ResourceRecordSet": { + "Name": "${params.domain}", + "Type": "${params.recordType}", + "TTL": ${params.ttl}, + "ResourceRecords": [{ + "Value": "${params.value}" + }] + } + }] +} +""" + + // Apply Route53 change + def changeId = sh( + script: """ + aws route53 change-resource-record-sets \\ + --hosted-zone-id ${params.hostedZoneId} \\ + --change-batch '${changeBatch}' \\ + --profile ${params.profile} \\ + --region ${params.region} \\ + --query 'ChangeInfo.Id' \\ + --output text + """, + returnStdout: true + ).trim() + + if (changeId) { + openshiftTools.log('INFO', "Route53 record created/updated successfully. Change ID: ${changeId}") + + // Wait for change to propagate + openshiftTools.log('INFO', 'Waiting for DNS change to propagate...') + sh """ + aws route53 wait resource-record-sets-changed \\ + --id ${changeId} \\ + --profile ${params.profile} \\ + --region ${params.region} || true + """ + + return true + } else { + openshiftTools.log('WARN', 'No change ID returned from Route53') + return false + } + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to create Route53 record: ${e.message}") + return false + } +} + +/** + * Gets the Route53 hosted zone ID for a domain. + * + * @param config Map containing: + * - domain: Domain name (required) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * + * @return Hosted zone ID if found, null otherwise + */ +def getHostedZoneId(Map config = [:]) { + def params = [ + region: 'us-east-2', + profile: 'percona-dev-admin' + ] + config + + if (!params.domain) { + error 'Missing required parameter: domain' + } + + openshiftTools.log('DEBUG', "Looking for hosted zone for domain: ${params.domain}") + + try { + // Extract base domain (e.g., 'cd.percona.com' from 'console-test.cd.percona.com') + def domainParts = params.domain.split('\\.') + def possibleZones = [] + + // Build list of possible zones from most specific to least specific + for (int i = 0; i < domainParts.size() - 1; i++) { + possibleZones.add(domainParts[i..-1].join('.')) + } + + openshiftTools.log('DEBUG', "Checking possible zones: ${possibleZones}") + + for (zone in possibleZones) { + def zoneId = sh( + script: """ + aws route53 list-hosted-zones-by-name \\ + --profile ${params.profile} \\ + --region ${params.region} \\ + --query "HostedZones[?Name=='${zone}.'].Id" \\ + --output text 2>/dev/null | head -1 + """, + returnStdout: true + ).trim() + + if (zoneId && zoneId != 'None') { + // Extract just the ID part (remove '/hostedzone/' prefix) + zoneId = zoneId.replaceAll('/hostedzone/', '') + openshiftTools.log('INFO', "Found hosted zone ${zone} with ID: ${zoneId}") + return zoneId + } + } + + openshiftTools.log('WARN', "No hosted zone found for domain: ${params.domain}") + return null + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to get hosted zone ID: ${e.message}") + return null + } +} + +/** + * Requests a new ACM certificate with DNS validation. + * + * Creates a new ACM certificate request and automatically creates the + * required DNS validation records in Route53. + * + * @param config Map containing: + * - domain: Domain name for certificate (required) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * - alternativeNames: Additional domain names (optional) + * - waitForValidation: Wait for certificate to be validated (optional, default: true) + * + * @return Certificate ARN if successful, null otherwise + */ +def requestACMCertificate(Map config = [:]) { + def params = [ + region: 'us-east-2', + profile: 'percona-dev-admin', + waitForValidation: true, + alternativeNames: [] + ] + config + + if (!params.domain) { + error 'Missing required parameter: domain' + } + + openshiftTools.log('INFO', "Requesting ACM certificate for domain: ${params.domain}") + + try { + // Build AWS CLI command for certificate request + def cmdArgs = [ + "aws acm request-certificate", + "--domain-name ${params.domain}", + "--validation-method DNS", + "--region ${params.region}", + "--profile ${params.profile}", + "--query 'CertificateArn'", + "--output text" + ] + + // Add alternative names if provided + if (params.alternativeNames && params.alternativeNames.size() > 0) { + cmdArgs.add(2, "--subject-alternative-names ${params.alternativeNames.join(' ')}") + } + + def certificateArn = sh( + script: cmdArgs.join(' \\\n '), + returnStdout: true + ).trim() + + if (!certificateArn || certificateArn == 'None') { + error 'Failed to request ACM certificate' + } + + openshiftTools.log('INFO', "Certificate requested: ${certificateArn}") + + // Wait for validation details to be available + sleep(time: 10, unit: 'SECONDS') + + // Get DNS validation records using AWS CLI + def validationRecords = sh( + script: """ + aws acm describe-certificate \\ + --certificate-arn ${certificateArn} \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --query 'Certificate.DomainValidationOptions[*].[DomainName,ResourceRecord.Name,ResourceRecord.Value]' \\ + --output json + """, + returnStdout: true + ).trim() + + def records = readJSON(text: validationRecords) + + // Create DNS validation records in Route53 + records.each { record -> + def domain = record[0] + def recordName = record[1] + def recordValue = record[2] + + if (recordName && recordValue) { + openshiftTools.log('INFO', "Creating validation record for ${domain}: ${recordName}") + + // Find the hosted zone for this domain + def baseDomain = domain.startsWith('*.') ? domain.substring(2) : domain + def hostedZoneId = getHostedZoneId([domain: baseDomain, region: params.region, profile: params.profile]) + + if (hostedZoneId) { + // Create the validation CNAME record using AWS CLI + def changeFile = "/tmp/acm-validation-${BUILD_NUMBER}-${System.currentTimeMillis()}.json" + writeFile file: changeFile, text: """ +{ + "Changes": [{ + "Action": "UPSERT", + "ResourceRecordSet": { + "Name": "${recordName}", + "Type": "CNAME", + "TTL": 300, + "ResourceRecords": [{ + "Value": "${recordValue}" + }] + } + }] +} +""" + + sh """ + aws route53 change-resource-record-sets \\ + --hosted-zone-id ${hostedZoneId} \\ + --change-batch file://${changeFile} \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --output json > /dev/null + + rm -f ${changeFile} + """ + + openshiftTools.log('INFO', "Created validation record: ${recordName} -> ${recordValue}") + } else { + openshiftTools.log('WARN', "Could not find hosted zone for domain: ${baseDomain}") + } + } + } + + if (params.waitForValidation) { + openshiftTools.log('INFO', 'Waiting for certificate validation...') + + // Wait for certificate to be validated (up to 10 minutes) + def maxAttempts = 20 + def attempt = 0 + def validated = false + + while (attempt < maxAttempts && !validated) { + sleep(time: 30, unit: 'SECONDS') + + // Check certificate status using AWS CLI + def status = sh( + script: """ + aws acm describe-certificate \\ + --certificate-arn ${certificateArn} \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --query 'Certificate.Status' \\ + --output text + """, + returnStdout: true + ).trim() + + if (status == 'ISSUED') { + validated = true + openshiftTools.log('INFO', 'Certificate validated successfully!') + } else { + attempt++ + openshiftTools.log('INFO', "Certificate status: ${status}, waiting... (${attempt}/${maxAttempts})") + } + } + + if (!validated) { + openshiftTools.log('WARN', 'Certificate validation timed out. It may still validate in the background.') + } + } + + return certificateArn + + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to request ACM certificate: ${e.message}") + return null + } +} + +/** + * Validates domain ownership in AWS ACM. + * + * Checks if a domain has a valid certificate in ACM and if DNS validation is complete. + * + * @param config Map containing: + * - certificateArn: ACM certificate ARN (required) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * + * @return Map containing validation status and details + */ +def validateDomain(Map config) { + def params = [ + region: 'us-east-2', + profile: 'percona-dev-admin' + ] + config + + if (!params.certificateArn) { + error 'Missing required parameter: certificateArn' + } + + openshiftTools.log('INFO', "Validating ACM certificate: ${params.certificateArn}") + + try { + def certInfo = sh( + script: """ + aws acm describe-certificate \\ + --certificate-arn ${params.certificateArn} \\ + --region ${params.region} \\ + --profile ${params.profile} \\ + --query 'Certificate.{Status:Status,ValidationStatus:DomainValidationOptions[0].ValidationStatus,Domain:DomainName,Type:Type,InUseBy:InUseBy}' \\ + --output json + """, + returnStdout: true + ).trim() + + def certData = readJSON text: certInfo + + def isValid = certData.Status == 'ISSUED' && + (certData.ValidationStatus == 'SUCCESS' || certData.ValidationStatus == null) + + openshiftTools.log('INFO', "Certificate validation status: ${certData.Status}") + + return [ + valid: isValid, + status: certData.Status, + validationStatus: certData.ValidationStatus, + domain: certData.Domain, + type: certData.Type, + inUseBy: certData.InUseBy ?: [] + ] + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to validate domain: ${e.message}") + return [ + valid: false, + error: e.message + ] + } +} + +/** + * Sets up complete AWS ACM SSL configuration for services. + * + * This is a high-level function that orchestrates the entire ACM setup process + * for LoadBalancer services including DNS configuration. + * + * @param config Map containing: + * - clusterName: Name of the OpenShift cluster (required) + * - baseDomain: Base domain (required) + * - kubeconfig: Path to kubeconfig file (required) + * - region: AWS region (optional, default: 'us-east-2') + * - profile: AWS profile (optional, default: 'percona-dev-admin') + * - services: List of services to configure (optional) + * + * @return Map containing setup results + */ +def setupACM(Map config) { + def params = [ + region: 'us-east-2', + profile: 'percona-dev-admin', + services: [] + ] + config + + def required = ['clusterName', 'baseDomain', 'kubeconfig'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + openshiftTools.log('INFO', "Setting up AWS ACM SSL for cluster ${params.clusterName}") + + def results = [ + wildcardCert: null, + services: [:], + dns: [:], + errors: [] + ] + + try { + // Step 1: Find or request wildcard certificate for the base domain + def wildcardDomain = "*.${params.baseDomain}" + results.wildcardCert = findACMCertificate([ + domain: wildcardDomain, + region: params.region, + profile: params.profile + ]) + + if (!results.wildcardCert && params.autoCreateCertificate != false) { + openshiftTools.log('INFO', "No wildcard certificate found for ${wildcardDomain}, requesting new certificate...") + + // Request a new wildcard certificate with DNS validation + results.wildcardCert = requestACMCertificate([ + domain: wildcardDomain, + region: params.region, + profile: params.profile, + waitForValidation: true + ]) + + if (results.wildcardCert) { + openshiftTools.log('INFO', "Successfully created wildcard certificate: ${results.wildcardCert}") + } else { + results.errors.add("Failed to create wildcard certificate for ${wildcardDomain}") + openshiftTools.log('WARN', "Could not create wildcard ACM certificate. Services will need individual certificates.") + } + } else if (!results.wildcardCert) { + results.errors.add("No wildcard certificate found for ${wildcardDomain}") + openshiftTools.log('WARN', "No wildcard ACM certificate found. Services will need individual certificates.") + } + + // Step 2: Process each service + params.services.each { service -> + try { + def serviceResult = [ + configured: false, + certificateArn: null, + domain: null, + loadBalancer: null + ] + + // Use wildcard cert or find/request specific cert + serviceResult.certificateArn = results.wildcardCert + + if (!serviceResult.certificateArn && service.domain) { + // Try to find existing certificate for specific domain + serviceResult.certificateArn = findACMCertificate([ + domain: service.domain, + region: params.region, + profile: params.profile + ]) + + // Request new certificate if not found and auto-create is enabled + if (!serviceResult.certificateArn && params.autoCreateCertificate != false) { + openshiftTools.log('INFO', "Requesting certificate for ${service.domain}...") + serviceResult.certificateArn = requestACMCertificate([ + domain: service.domain, + region: params.region, + profile: params.profile, + waitForValidation: true + ]) + } + } + + if (serviceResult.certificateArn) { + // Apply certificate to LoadBalancer + def applied = applyACMToLoadBalancer([ + namespace: service.namespace, + serviceName: service.name, + certificateArn: serviceResult.certificateArn, + kubeconfig: params.kubeconfig + ]) + + if (applied) { + // Get LoadBalancer hostname + serviceResult.loadBalancer = sh( + script: """ + export KUBECONFIG=${params.kubeconfig} + oc get svc ${service.name} -n ${service.namespace} \\ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + """, + returnStdout: true + ).trim() + + if (serviceResult.loadBalancer && service.domain) { + // Create DNS record + def dnsCreated = createRoute53Record([ + domain: service.domain, + value: serviceResult.loadBalancer, + region: params.region, + profile: params.profile + ]) + + if (dnsCreated) { + serviceResult.domain = service.domain + serviceResult.configured = true + results.dns[service.domain] = serviceResult.loadBalancer + } + } + } + } + + results.services[service.name] = serviceResult + } catch (Exception e) { + results.errors.add("Failed to configure service ${service.name}: ${e.message}") + } + } + + openshiftTools.log('INFO', 'AWS ACM SSL setup completed') + } catch (Exception e) { + openshiftTools.log('ERROR', "ACM setup failed: ${e.message}") + results.errors.add(e.message) + } + + return results +} + +return this \ No newline at end of file diff --git a/vars/openshiftCluster.groovy b/vars/openshiftCluster.groovy index 4340f6720f..b1aea580d3 100644 --- a/vars/openshiftCluster.groovy +++ b/vars/openshiftCluster.groovy @@ -102,7 +102,14 @@ def create(Map config) { pmmHelmChartVersion: '1.4.7', pmmImageRepository: 'percona/pmm-server', pmmNamespace: 'pmm-monitoring', - pmmAdminPassword: '' // Default to auto-generation + pmmAdminPassword: '', // Default to auto-generation + // SSL Configuration defaults + enableSSL: false, + sslMethod: 'acm', + sslEmail: 'admin@percona.com', + useStaging: false, + consoleCustomDomain: '', + pmmCustomDomain: '' ] + config // Use provided credentials or fall back to environment variables @@ -212,7 +219,15 @@ def create(Map config) { // Deploy Percona Monitoring and Management if enabled if (params.deployPMM) { env.KUBECONFIG = "${clusterDir}/auth/kubeconfig" - def pmmInfo = deployPMM(params) + + // Pass SSL configuration to PMM deployment + def pmmParams = params + [ + clusterName: params.clusterName, + baseDomain: params.baseDomain, + awsRegion: params.awsRegion + ] + + def pmmInfo = deployPMM(pmmParams) metadata.pmmDeployed = true metadata.pmmImageTag = params.pmmImageTag @@ -639,6 +654,12 @@ def deployPMM(Map params) { // Install Helm if not already installed openshiftTools.installHelm() + // Determine service type based on SSL configuration + // When SSL is enabled, PMM needs LoadBalancer for ACM certificate + def serviceType = params.enableSSL ? 'LoadBalancer' : 'ClusterIP' + + openshiftTools.log('INFO', "PMM service type: ${serviceType} (SSL enabled: ${params.enableSSL})") + sh """ export PATH="\$HOME/.local/bin:\$PATH" # Create namespace @@ -664,7 +685,7 @@ def deployPMM(Map params) { --namespace ${params.pmmNamespace} \ --version ${params.pmmHelmChartVersion} \ --set platform=openshift \ - --set service.type=ClusterIP \ + --set service.type=${serviceType} \ --set image.repository=${params.pmmImageRepository} \ --set image.tag=${params.pmmImageTag}""" @@ -710,7 +731,7 @@ def deployPMM(Map params) { returnStdout: true ).trim() - return [ + def result = [ url: "https://${pmmUrl}", username: 'admin', password: actualPassword, @@ -718,6 +739,74 @@ def deployPMM(Map params) { // Indicate if password was auto-generated passwordGenerated: !params.pmmAdminPassword || params.pmmAdminPassword == '' ] + + // Configure SSL with ACM if enabled + if (params.enableSSL && serviceType == 'LoadBalancer') { + openshiftTools.log('INFO', 'Configuring ACM SSL for PMM LoadBalancer...') + + // Wait for LoadBalancer to be ready + sleep(time: 30, unit: 'SECONDS') + + // Get LoadBalancer hostname + def lbHostname = sh( + script: """ + export PATH="\$HOME/.local/bin:\$PATH" + oc get svc monitoring-service -n ${params.pmmNamespace} -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + """, + returnStdout: true + ).trim() + + if (lbHostname) { + openshiftTools.log('INFO', "PMM LoadBalancer hostname: ${lbHostname}") + + // Find ACM certificate for the domain + def pmmDomain = params.pmmCustomDomain ?: "pmm-${params.clusterName}.${params.baseDomain}" + def wildcardDomain = "*.${params.baseDomain}" + + // Note: This requires AWS credentials to be available + def acmArn = awsCertificates.findACMCertificate([ + domain: wildcardDomain, + region: params.awsRegion ?: 'us-east-2' + ]) + + if (acmArn) { + openshiftTools.log('INFO', "Found ACM certificate: ${acmArn}") + + // Apply ACM certificate to LoadBalancer + def acmApplied = awsCertificates.applyACMToLoadBalancer([ + namespace: params.pmmNamespace, + serviceName: 'monitoring-service', + certificateArn: acmArn, + kubeconfig: env.KUBECONFIG + ]) + + if (acmApplied) { + // Create Route53 DNS record + def dnsCreated = awsCertificates.createRoute53Record([ + domain: pmmDomain, + value: lbHostname, + region: params.awsRegion ?: 'us-east-2' + ]) + + if (dnsCreated) { + result.sslDomain = pmmDomain + result.sslUrl = "https://${pmmDomain}" + openshiftTools.log('INFO', "PMM SSL configured successfully at: https://${pmmDomain}") + } else { + openshiftTools.log('WARN', 'Failed to create Route53 DNS record for PMM') + } + } else { + openshiftTools.log('WARN', 'Failed to apply ACM certificate to PMM LoadBalancer') + } + } else { + openshiftTools.log('WARN', "No ACM certificate found for ${wildcardDomain}") + } + } else { + openshiftTools.log('WARN', 'LoadBalancer hostname not available yet') + } + } + + return result } /** diff --git a/vars/openshiftSSL.groovy b/vars/openshiftSSL.groovy new file mode 100644 index 0000000000..2e47df0a7d --- /dev/null +++ b/vars/openshiftSSL.groovy @@ -0,0 +1,568 @@ +/** + * OpenShift SSL certificate management library for automated certificate provisioning. + * + * This library provides comprehensive SSL/TLS certificate management for OpenShift clusters, + * supporting both Let's Encrypt (via cert-manager) and AWS ACM certificate providers. + * + * @since 1.0.0 + */ + +/** + * Installs cert-manager operator in the OpenShift cluster. + * + * cert-manager is a Kubernetes add-on to automate the management and issuance of + * TLS certificates from various issuing sources including Let's Encrypt. + * + * @param config Map containing: + * - kubeconfig: Path to kubeconfig file (required) + * - namespace: Target namespace (optional, default: 'openshift-operators') + * - channel: Operator channel (optional, default: 'stable') + * - timeout: Installation timeout in seconds (optional, default: 300) + * + * @return Boolean indicating installation success + */ +def installCertManager(Map config = [:]) { + def params = [ + namespace: 'openshift-operators', + channel: 'stable', + timeout: 300 + ] + config + + if (!params.kubeconfig) { + error 'Missing required parameter: kubeconfig' + } + + openshiftTools.log('INFO', 'Installing cert-manager operator...') + + try { + // Set kubeconfig for all operations + env.KUBECONFIG = params.kubeconfig + + // Check if cert-manager is already installed + def isInstalled = sh( + script: "oc get csv -n ${params.namespace} 2>/dev/null | grep -q cert-manager", + returnStatus: true + ) == 0 + + if (isInstalled) { + openshiftTools.log('INFO', 'cert-manager operator is already installed') + return true + } + + // Create subscription for cert-manager operator + sh """ + cat << 'EOF' | oc apply -f - +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: cert-manager + namespace: ${params.namespace} +spec: + channel: ${params.channel} + installPlanApproval: Automatic + name: cert-manager + source: community-operators + sourceNamespace: openshift-marketplace +EOF + """ + + // Wait for operator installation + openshiftTools.log('INFO', 'Waiting for cert-manager operator to be ready...') + sh """ + sleep 30 + oc wait --for=condition=Succeeded csv \\ + -n ${params.namespace} \\ + -l operators.coreos.com/cert-manager.openshift-operators \\ + --timeout=${params.timeout}s + """ + + openshiftTools.log('INFO', 'cert-manager operator installed successfully') + return true + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to install cert-manager: ${e.message}") + return false + } +} + +/** + * Creates Let's Encrypt ClusterIssuers for certificate generation. + * + * Creates both staging and production issuers to support testing and production deployments. + * Staging issuer should be used for testing to avoid Let's Encrypt rate limits. + * + * @param config Map containing: + * - email: Email address for Let's Encrypt registration (required) + * - kubeconfig: Path to kubeconfig file (required) + * - createStaging: Create staging issuer (optional, default: true) + * - createProduction: Create production issuer (optional, default: true) + * + * @return Boolean indicating success + */ +def createClusterIssuers(Map config = [:]) { + def params = [ + createStaging: true, + createProduction: true + ] + config + + if (!params.email || !params.kubeconfig) { + error 'Missing required parameters: email and kubeconfig' + } + + env.KUBECONFIG = params.kubeconfig + openshiftTools.log('INFO', "Creating Let's Encrypt ClusterIssuers with email: ${params.email}") + + try { + def issuersYaml = "" + + if (params.createStaging) { + issuersYaml += """ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + email: ${params.email} + server: https://acme-staging-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-staging-account-key + solvers: + - http01: + ingress: + class: openshift-default +--- +""" + } + + if (params.createProduction) { + issuersYaml += """ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + email: ${params.email} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-production-account-key + solvers: + - http01: + ingress: + class: openshift-default +""" + } + + // Apply ClusterIssuers + sh """ + cat << 'EOF' | oc apply -f - +${issuersYaml} +EOF + """ + + // Wait for ClusterIssuers to be ready + openshiftTools.log('INFO', 'Waiting for ClusterIssuers to be ready...') + sleep(time: 10, unit: 'SECONDS') + + def issuersToCheck = [] + if (params.createStaging) issuersToCheck.add('letsencrypt-staging') + if (params.createProduction) issuersToCheck.add('letsencrypt-production') + + issuersToCheck.each { issuer -> + sh """ + oc wait --for=condition=Ready clusterissuer ${issuer} --timeout=60s || true + """ + } + + openshiftTools.log('INFO', 'ClusterIssuers created successfully') + return true + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to create ClusterIssuers: ${e.message}") + return false + } +} + +/** + * Creates a certificate request using cert-manager. + * + * @param config Map containing: + * - namespace: Kubernetes namespace (required) + * - name: Certificate name (required) + * - domain: Domain name for the certificate (required) + * - kubeconfig: Path to kubeconfig file (required) + * - issuer: ClusterIssuer name (optional, default: 'letsencrypt-production') + * - secretName: TLS secret name (optional, default: '${name}-tls') + * - timeout: Certificate ready timeout (optional, default: 300) + * + * @return Map containing certificate details or null on failure + */ +def createCertificate(Map config) { + def params = [ + issuer: 'letsencrypt-production', + timeout: 300 + ] + config + + def required = ['namespace', 'name', 'domain', 'kubeconfig'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + params.secretName = params.secretName ?: "${params.name}-tls" + env.KUBECONFIG = params.kubeconfig + + openshiftTools.log('INFO', "Creating certificate '${params.name}' for domain '${params.domain}'") + + try { + // Create certificate resource + sh """ + cat << 'EOF' | oc apply -f - +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ${params.name} + namespace: ${params.namespace} +spec: + secretName: ${params.secretName} + issuerRef: + name: ${params.issuer} + kind: ClusterIssuer + dnsNames: + - ${params.domain} +EOF + """ + + // Wait for certificate to be ready + openshiftTools.log('INFO', "Waiting for certificate to be ready (timeout: ${params.timeout}s)...") + def ready = sh( + script: """ + oc wait --for=condition=Ready \\ + certificate/${params.name} \\ + -n ${params.namespace} \\ + --timeout=${params.timeout}s + """, + returnStatus: true + ) == 0 + + if (!ready) { + // Check certificate status for debugging + sh """ + echo "Certificate status:" + oc describe certificate ${params.name} -n ${params.namespace} + echo "Challenge status:" + oc get challenges -n ${params.namespace} + """ + error "Certificate not ready after ${params.timeout} seconds" + } + + openshiftTools.log('INFO', "Certificate '${params.name}' created successfully") + + return [ + name: params.name, + namespace: params.namespace, + secretName: params.secretName, + domain: params.domain, + issuer: params.issuer + ] + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to create certificate: ${e.message}") + return null + } +} + +/** + * Applies a certificate to an OpenShift route. + * + * @param config Map containing: + * - namespace: Route namespace (required) + * - routeName: Name of the route (required) + * - certificateSecret: Name of the certificate secret (required) + * - domain: Custom domain for the route (required) + * - kubeconfig: Path to kubeconfig file (required) + * - serviceName: Backend service name (optional, will try to detect) + * - servicePort: Backend service port (optional, default: 'https') + * - insecurePolicy: Insecure traffic policy (optional, default: 'Redirect') + * + * @return Boolean indicating success + */ +def applyCertificateToRoute(Map config) { + def params = [ + servicePort: 'https', + insecurePolicy: 'Redirect' + ] + config + + def required = ['namespace', 'routeName', 'certificateSecret', 'domain', 'kubeconfig'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + env.KUBECONFIG = params.kubeconfig + openshiftTools.log('INFO', "Applying certificate to route '${params.routeName}' in namespace '${params.namespace}'") + + try { + // Extract certificates from secret + sh """ + mkdir -p /tmp/certs-${BUILD_NUMBER} + + # Extract certificate and key + oc get secret ${params.certificateSecret} -n ${params.namespace} \\ + -o jsonpath='{.data.tls\\.crt}' | base64 -d > /tmp/certs-${BUILD_NUMBER}/tls.crt + oc get secret ${params.certificateSecret} -n ${params.namespace} \\ + -o jsonpath='{.data.tls\\.key}' | base64 -d > /tmp/certs-${BUILD_NUMBER}/tls.key + + # Get service CA for reencrypt routes + oc get cm -n openshift-config-managed service-ca \\ + -o jsonpath='{.data.ca-bundle\\.crt}' > /tmp/certs-${BUILD_NUMBER}/service-ca.crt + """ + + // Detect service name if not provided + if (!params.serviceName) { + params.serviceName = sh( + script: """ + oc get route ${params.routeName} -n ${params.namespace} \\ + -o jsonpath='{.spec.to.name}' 2>/dev/null || echo "" + """, + returnStdout: true + ).trim() + + if (!params.serviceName) { + error "Could not detect service name for route ${params.routeName}" + } + } + + // Delete existing route if it exists + sh """ + oc delete route ${params.routeName} -n ${params.namespace} --ignore-not-found=true + """ + + // Create route with certificate + sh """ + oc create route reencrypt ${params.routeName} \\ + --service=${params.serviceName} \\ + --cert=/tmp/certs-${BUILD_NUMBER}/tls.crt \\ + --key=/tmp/certs-${BUILD_NUMBER}/tls.key \\ + --dest-ca-cert=/tmp/certs-${BUILD_NUMBER}/service-ca.crt \\ + --hostname=${params.domain} \\ + -n ${params.namespace} \\ + --port=${params.servicePort} \\ + --insecure-policy=${params.insecurePolicy} + """ + + // Cleanup temp files + sh "rm -rf /tmp/certs-${BUILD_NUMBER}" + + openshiftTools.log('INFO', "Certificate applied to route '${params.routeName}' successfully") + return true + } catch (Exception e) { + openshiftTools.log('ERROR', "Failed to apply certificate to route: ${e.message}") + sh "rm -rf /tmp/certs-${BUILD_NUMBER} || true" + return false + } +} + +/** + * Verifies SSL certificate configuration for a domain. + * + * @param config Map containing: + * - domain: Domain to verify (required) + * - timeout: Connection timeout in seconds (optional, default: 10) + * - retries: Number of verification attempts (optional, default: 3) + * - retryDelay: Delay between retries in seconds (optional, default: 10) + * + * @return Map containing verification results + */ +def verifyCertificate(Map config) { + def params = [ + timeout: 10, + retries: 3, + retryDelay: 10 + ] + config + + if (!params.domain) { + error 'Missing required parameter: domain' + } + + openshiftTools.log('INFO', "Verifying SSL certificate for ${params.domain}") + + def verified = false + def certInfo = [:] + def lastError = '' + + for (int i = 0; i < params.retries; i++) { + if (i > 0) { + openshiftTools.log('INFO', "Retry ${i}/${params.retries - 1} after ${params.retryDelay}s...") + sleep(time: params.retryDelay, unit: 'SECONDS') + } + + try { + // Get certificate details + def certDetails = sh( + script: """ + echo | openssl s_client -connect ${params.domain}:443 \\ + -servername ${params.domain} 2>/dev/null | \\ + openssl x509 -noout -subject -issuer -dates 2>/dev/null || echo "FAILED" + """, + returnStdout: true + ).trim() + + if (certDetails && certDetails != 'FAILED') { + // Parse certificate details + certDetails.split('\n').each { line -> + if (line.startsWith('subject=')) { + certInfo.subject = line.replace('subject=', '').trim() + } else if (line.startsWith('issuer=')) { + certInfo.issuer = line.replace('issuer=', '').trim() + } else if (line.startsWith('notBefore=')) { + certInfo.notBefore = line.replace('notBefore=', '').trim() + } else if (line.startsWith('notAfter=')) { + certInfo.notAfter = line.replace('notAfter=', '').trim() + } + } + + // Test HTTPS connectivity + def httpStatus = sh( + script: """ + curl -I https://${params.domain} --max-time ${params.timeout} \\ + -o /dev/null -w '%{http_code}' -s + """, + returnStdout: true + ).trim() + + certInfo.httpStatus = httpStatus + certInfo.verified = httpStatus.startsWith('2') || httpStatus.startsWith('3') + + if (certInfo.verified) { + verified = true + openshiftTools.log('INFO', "SSL certificate verified successfully for ${params.domain}") + break + } else { + lastError = "HTTP status: ${httpStatus}" + } + } else { + lastError = 'Failed to retrieve certificate information' + } + } catch (Exception e) { + lastError = e.message + } + } + + if (!verified) { + openshiftTools.log('WARN', "SSL certificate verification failed for ${params.domain}: ${lastError}") + } + + return [ + domain: params.domain, + verified: verified, + details: certInfo, + error: verified ? null : lastError + ] +} + +/** + * Sets up complete Let's Encrypt SSL configuration for an OpenShift cluster. + * + * This is a high-level function that orchestrates the entire Let's Encrypt setup process. + * + * @param config Map containing: + * - clusterName: Name of the OpenShift cluster (required) + * - baseDomain: Base domain (required) + * - email: Email for Let's Encrypt (required) + * - kubeconfig: Path to kubeconfig file (required) + * - useStaging: Use staging certificates (optional, default: false) + * - configureConsole: Configure console SSL (optional, default: true) + * - configurePMM: Configure PMM SSL if deployed (optional, default: true) + * + * @return Map containing setup results + */ +def setupLetsEncrypt(Map config) { + def params = [ + useStaging: false, + configureConsole: true, + configurePMM: true + ] + config + + def required = ['clusterName', 'baseDomain', 'email', 'kubeconfig'] + required.each { param -> + if (!params.containsKey(param) || !params[param]) { + error "Missing required parameter: ${param}" + } + } + + openshiftTools.log('INFO', "Setting up Let's Encrypt SSL for cluster ${params.clusterName}") + + def results = [ + certManager: false, + clusterIssuers: false, + consoleCert: null, + pmmCert: null, + errors: [] + ] + + try { + // Step 1: Install cert-manager + results.certManager = installCertManager([ + kubeconfig: params.kubeconfig + ]) + + if (!results.certManager) { + results.errors.add('Failed to install cert-manager') + return results + } + + // Step 2: Create ClusterIssuers + results.clusterIssuers = createClusterIssuers([ + email: params.email, + kubeconfig: params.kubeconfig + ]) + + if (!results.clusterIssuers) { + results.errors.add('Failed to create ClusterIssuers') + return results + } + + def issuer = params.useStaging ? 'letsencrypt-staging' : 'letsencrypt-production' + + // Step 3: Configure OpenShift Console SSL + if (params.configureConsole) { + def consoleDomain = "console-${params.clusterName}.${params.baseDomain}" + + results.consoleCert = createCertificate([ + namespace: 'openshift-console', + name: 'console-cert', + domain: consoleDomain, + kubeconfig: params.kubeconfig, + issuer: issuer + ]) + + if (results.consoleCert) { + applyCertificateToRoute([ + namespace: 'openshift-console', + routeName: 'console', + certificateSecret: results.consoleCert.secretName, + domain: consoleDomain, + kubeconfig: params.kubeconfig, + serviceName: 'console', + servicePort: 'https' + ]) + + // Verify console certificate + def verification = verifyCertificate([ + domain: consoleDomain + ]) + results.consoleCert.verified = verification.verified + } + } + + openshiftTools.log('INFO', 'Let\'s Encrypt SSL setup completed') + } catch (Exception e) { + openshiftTools.log('ERROR', "Let's Encrypt setup failed: ${e.message}") + results.errors.add(e.message) + } + + return results +} + +return this \ No newline at end of file From bc8dd0329e2b61ef03b5e6cc4fe3733b81259274 Mon Sep 17 00:00:00 2001 From: Anderson Nogueira Date: Tue, 26 Aug 2025 08:39:09 +0200 Subject: [PATCH 2/2] Add missing newlines at end of files - Add newline to vars/openshiftSSL.groovy - Add newline to vars/awsCertificates.groovy Per code style guidelines, all files should end with a newline character. --- vars/awsCertificates.groovy | 2 +- vars/openshiftSSL.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vars/awsCertificates.groovy b/vars/awsCertificates.groovy index db3fddae54..ca901035dc 100644 --- a/vars/awsCertificates.groovy +++ b/vars/awsCertificates.groovy @@ -715,4 +715,4 @@ def setupACM(Map config) { return results } -return this \ No newline at end of file +return this diff --git a/vars/openshiftSSL.groovy b/vars/openshiftSSL.groovy index 2e47df0a7d..8bd46b8b43 100644 --- a/vars/openshiftSSL.groovy +++ b/vars/openshiftSSL.groovy @@ -565,4 +565,4 @@ def setupLetsEncrypt(Map config) { return results } -return this \ No newline at end of file +return this