Upgrade CNFs from 2.2.1 to 2.3.0 using F5 Lifecycle Operator

This document describes how to upgrade the F5 Lifecycle Operator (FLO), migrate CPCL ConfigMaps to Secrets, upgrade the CNE Instance, and apply the License custom resource.

Important

Ensure that license content (JWT values) are removed from any values files before sharing or committing them.

Upgrade the F5 Lifecycle Operator (FLO)

  1. Create or update the FLO values file.

    Example: flo-values_2.3.0.yaml

    global:
    imagePullSecrets:
        - name: far-secret
    certmgr:
        clusterIssuer: arm-ca-cluster-issuer
    
    image:
    repository: repo.f5.com/images
    pullPolicy: IfNotPresent
    
    f5-spk-crds-common:
    versionValidator:
        image:
        repository: repo.f5.com/images
    
    f5-spk-crds-service-proxy:
    versionValidator:
        image:
        repository: repo.f5.com/images
    
    f5-ipam-operator:
    image:
        repository: repo.f5.com/images
        pullPolicy: IfNotPresent
    namespace: default
    nameOverride: f5-ipam-operator
    fullnameOverride: f5-ipam-operator
    
    sharedComponentNamespace: f5-utils
    
  2. Execute the Helm upgrade using the values file.

    $ helm upgrade f5-lifecycle-operator \
    tar/f5-lifecycle-operator-v2.21.13-0.0.26.tgz \
    -f arm/tmp/flo-values_2.3.0.yaml
    
  3. Verify the Upgrade.

    $ helm list
    

Run the script to migrate cpcl configuration from ConfigMaps to Kubernetes Secrets

Before upgrading the CWC, run the below script to migrate cpcl-config-cm and cpcl-key-cm ConfigMaps to Secrets. This is required as part of the upgrade process since the new version of CWC uses Secrets instead of ConfigMaps to store CPCL configuration and key.

  1. Copy and save the script file as migrate-cpcl-to-secrets.sh in the release directory.

    $ cat migrate-cpcl-to-secrets.sh
    
    #!/bin/bash
    # Migration script: Populate cpcl-config-secret and cpcl-key-secret from ConfigMaps
    # Usage: ./migrate-cpcl-to-secrets.sh [-n NAMESPACE] [--dry-run]
    
    set -euo pipefail
    
    NAMESPACE="f5-utils"
    DRY_RUN=""
    
    while [[ $# -gt 0 ]]; do
    case $1 in
        -n|--namespace) NAMESPACE="$2"; shift 2 ;;
        --dry-run)      DRY_RUN="--dry-run=client"; shift ;;
        -h|--help)
        echo "Usage: $0 [-n NAMESPACE] [--dry-run]"
        echo "  -n, --namespace   Kubernetes namespace (default: cwc)"
        echo "  --dry-run         Preview without applying"
        exit 0
        ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
    done
    
    JWT_KEY_RAW=$(kubectl get cm cpcl-key-cm -n "${NAMESPACE}" -o jsonpath='{.data.jwt\.key}' 2>/dev/null)
    if [ -z "${JWT_KEY_RAW}" ]; then
    echo "ERROR: Could not read jwt.key from cpcl-key-cm in namespace ${NAMESPACE}"
    exit 1
    fi
    
    # Base64 encode the jwt.key value (no line wrapping)
    JWT_KEY_B64=$(echo -n "${JWT_KEY_RAW}" | base64 -w0)
    
    echo "  jwt.key length (raw): ${#JWT_KEY_RAW} chars"
    echo "  jwt.key length (b64): ${#JWT_KEY_B64} chars"
    
    cat <<EOF | kubectl apply ${DRY_RUN} -f -
    apiVersion: v1
    kind: Secret
    metadata:
    name: cpcl-key-secret
    namespace: ${NAMESPACE}
    labels:
        app.kubernetes.io/managed-by: cwc-license-controller
    type: Opaque
    data:
    jwt.key: ${JWT_KEY_B64}
    EOF
    
    echo "  ✓ cpcl-key-secret created/updated"
    echo ""
    
    # List of all fields in cpcl-config-cm to migrate
    CONFIG_FIELDS=(
        "custom_id"
        "digital_asset_version"
        "jwt"
        "licenseserver_root_ca_path"
        "log_level"
        "operation_mode"
        "signed_verify_cert_path"
        "teem_cert_url"
        "teem_entitlement_url"
        "teem_initial_config_url"
    )
    
    # Build the data section dynamically
    DATA_SECTION=""
    for field in "${CONFIG_FIELDS[@]}"; do
        VALUE=$(kubectl get cm cpcl-config-cm -n "${NAMESPACE}" -o jsonpath="{.data.${field}}" 2>/dev/null || true)
        if [ -z "${VALUE}" ]; then
        echo "  WARNING: Field '${field}' is empty or not found in cpcl-config-cm, encoding empty string"
        fi
        VALUE_B64=$(echo -n "${VALUE}" | base64 -w0)
        DATA_SECTION="${DATA_SECTION}  ${field}: ${VALUE_B64}"$'\n'
        echo "  ${field}: $(echo -n "${VALUE}" | head -c 60)... → (b64 ${#VALUE_B64} chars)"
    done
    
    cat <<EOF | kubectl apply ${DRY_RUN} -f -
    apiVersion: v1
    kind: Secret
    metadata:
        name: cpcl-config-secret
        namespace: ${NAMESPACE}
        labels:
            app.kubernetes.io/managed-by: cwc-license-controller
    type: Opaque
    data:
    ${DATA_SECTION}
    EOF
    
    echo "  ✓ cpcl-config-secret created/updated"
    echo ""
    
    if [ -z "${DRY_RUN}" ]; then
        echo "Verifying cpcl-key-secret..."
        VERIFY_KEY=$(kubectl get secret cpcl-key-secret -n "${NAMESPACE}" -o jsonpath='{.data.jwt\.key}' | base64 -d | head -c 80)
        echo "  jwt.key starts with: ${VERIFY_KEY}..."
    
    echo ""
    echo "Verifying cpcl-config-secret..."
    for field in "${CONFIG_FIELDS[@]}"; do
        VERIFY_VAL=$(kubectl get secret cpcl-config-secret -n "${NAMESPACE}" -o jsonpath="{.data.${field}}" | base64 -d | head -c 60)
        echo "  ${field}: ${VERIFY_VAL}..."
    done
    else
        echo "  Skipped (dry-run mode)"
    fi
    
    echo ""
    echo "=== Migration Complete ==="
    

    Note

    This script reads the cpcl-key-cm and cpcl-config-cm ConfigMaps, encodes the values in base64 format, and creates/updates the cpcl-key-secret and cpcl-config-secret Secrets in the specified namespace. It also includes a dry-run option to preview the changes without applying them.

  2. Using chmod +x make the file executable.

    $ chmod +x migrate-cpcl-to-secrets.sh
    
  3. Execute the script in the f5-utils namespace where the ConfigMaps are available.

    $ ./migrate-cpcl-to-secrets.sh -n f5-utils
    

    Sample Output

    jwt.key length (raw): 8618 chars
    jwt.key length (b64): 11492 chars
    secret/cpcl-key-secret created
        ✓ cpcl-key-secret created/updated
    
    custom_id: SPK Cluster... → (b64 16 chars)
    debug_telemetry: enabled... → (b64 12 chars)
    digital_asset_version: 1.2.0... → (b64 8 chars)
    jwt: eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6InYxIiwiamt1Ijoi... → (b64 1436 chars)
    WARNING: Field 'licenseserver_root_ca_path' is empty or not found in cpcl-config-cm, encoding empty string
    licenseserver_root_ca_path: ... → (b64 0 chars)
    log_level: debug... → (b64 8 chars)
    operation_mode: disconnected... → (b64 16 chars)
    signed_verify_cert_path: /etc/cm20/cpcl/private/key/jwt.key... → (b64 48 chars)
    teem_cert_url: https://product-tst.apis.f5networks.net/ee/v1... → (b64 60 chars)
    teem_entitlement_url: https://product-s-tst.apis.f5networks.net/ee/v1... → (b64 64 chars)
    teem_initial_config_url: https://product-s-tst.apis.f5networks.net/ee/v1... → (b64 64 chars)
    secret/cpcl-config-secret created
    ✓ cpcl-config-secret created/updated
    
    Verifying cpcl-key-secret...
    

Apply the CNE Instance

  1. Create the CNE Instance custom resource after the FLO upgrade completes.

    Example: cnegatewayclass-cr.yaml

    apiVersion: k8s.f5.com/v1
    kind: CNEInstance
    metadata:
    labels:
       app.kubernetes.io/name: f5-lifecycle-operator
       app.kubernetes.io/managed-by: kustomize
    name: svs-alpha-cneinstcr
    namespace: svs-alpha
    spec:
    wholeCluster: false
    product:
       type: CNF
       gatewayAPI: false
    manifestVersion: "2.3.0-3.2598.3-0.0.170"
    telemetry:
       loggingSubsystem:
         enabled: true
       metricSubsystem:
         enabled: true
    certificate:
       clusterIssuer: oss-issuer
    deploymentSize: "Small"
    registry:
       uri: "devrepo.f5.com"
       imagePullSecrets:
       - name: farimagesecret
       imagePullPolicy: Always
    networkAttachments:
       - "svs-alpha-cnf-intel710-external1-sriov"
       - "svs-alpha-cnf-intel710-internal-sriov"
    storageClassName: managed-nfs-storage
    # Features
    # CSRC Egress
    pseudoCNI:
       enabled: true
    # BGP
    dynamicRouting:
       enabled: true
    # AFM
    firewallACL:
       enabled: true
    # Core dump files
    coreCollection:
       enabled: true
    # CGNAT
    cgnat:
       enabled: true
    # IPSD
    intrusionPrevention:
       enabled: true
    # IP Intelligence
    ipIntelligence:
       enabled: true
    # Intelligent Load Balancer
    intelligentLB:
       enabled: false
    # DPU
    dpu:
       enabled: false
    # TMM Replicas (when not whole cluster or DPU mode)
    tmmReplicas: 2
    # Watch Namespaces
    #
    # watchNamespaces:
    #
    #- "tcpapp"
    #
    #- "udpapp"
    #
    #
    # Advanced Configuration
    advanced:
       maintenanceMode:
         enabled: false
       demoMode:
         enabled: false
       tmm:
         env:
         - name: SESSIONDB_EXTERNAL_SERVICE
           value: "f5-dssm-sentinel.svs-alpha.svc.cluster.local"
         - name: SESSIONDB_DISCOVERY_SENTINEL
           value: "true"
         - name: SSL_SERVERSIDE_STORE
           value: "/tls/tmm/mds/clt"
         - name: SSL_TRUSTED_CA_STORE
           value: "/tls/tmm/mds/clt"
         - name: TMM_MAPRES_VERBOSITY
           value: "debug"
         - name: TMM_MAPRES_USE_VETH_NAME  # Set to false to resolve the issue (BZ 1407137)
           value: "FALSE"
         - name: CONFIG_VIEWER_ENABLE
           value: "TRUE"
         - name: TMM_MAPRES_ADDL_VETHS_ON_DP
           value: "TRUE"
         - name: OPENSHIFT_VFIO_RESOURCE_1
           value: "ens25f0IntelE710Vfio"
         - name: OPENSHIFT_VFIO_RESOURCE_2
           value: "ens25f1IntelE710Vfio"
         - name: EXPORT_TMROUTED_LOGS
           value: "true"
         - name: EXPORT_BLOBD_LOGS
           value: "true"
       cneController:
         env:
         - name: ICNI20_ENABLED
           value: "true"
       envDiscovery:
         enabled: false
    
  2. Apply the resource:

    $ kubectl apply -f arm/tmp/cnegatewayclass-cr.yaml -n f5-alpha
    

Verify CNE Pods

  1. Verify Pods in f5-utils

    $ kubectl get pods -n f5-utils
    
  2. Verify Pods in f5-alpha

    $ kubectl get pods -n f5-alpha
    

Apply the License Custom Resource

  1. Apply the License custom resource after the CNE instance upgrade completes.

    Example: license-cr.yaml

    apiVersion: k8s.f5net.com/v1
    kind: License
    metadata:
    name: bnk-license
    spec:
    jwt: <REDACTED_JWT_VALUE>
    operationMode: connected
    
  2. Apply the license.

    $ kubectl apply -f license-cr.yaml