×

当在Google Cloud Platform (GCP)上运行的OpenShift Container Platform集群处于**GCP工作负载身份/联合身份**模式时,这意味着集群正在利用Google Cloud Platform (GCP)和OpenShift Container Platform的功能,以在应用程序级别应用GCP工作负载身份中的权限。

云凭据运营商(CCO)是在云提供商上运行的OpenShift Container Platform集群中默认安装的集群运营商。从OpenShift Container Platform 4.17开始,CCO支持针对使用GCP工作负载身份的OLM管理的运营商的工作流。

出于GCP工作负载身份的目的,CCO提供以下功能:

  • 检测它是否在启用了GCP工作负载身份的集群上运行

  • 检查CredentialsRequest对象中是否存在提供授予运营商访问GCP资源所需信息的字段

CCO可以通过扩展使用CredentialsRequest对象来半自动化此过程,该对象可以请求创建包含GCP工作负载身份工作流所需信息的Secrets

不建议使用自动批准更新的订阅,因为在更新之前可能需要进行权限更改。使用手动批准更新的订阅可确保管理员有机会验证更高版本的权限,采取必要的步骤,然后进行更新。

作为准备在OpenShift Container Platform 4.17及更高版本中与更新的CCO一起使用的运营商的运营商作者,您应该指示用户并添加代码以处理与早期CCO版本的差异,以及处理GCP工作负载身份令牌身份验证(如果您的运营商尚未启用)。推荐的方法是提供一个包含正确填充的GCP工作负载身份字段的CredentialsRequest对象,并让CCO为您创建Secret对象。

如果您计划支持早于4.17版本的OpenShift Container Platform集群,请考虑向用户提供有关如何使用CCO实用程序(ccoctl)手动创建包含启用GCP工作负载身份信息的密钥的说明。早期版本的CCO不知道集群上的GCP工作负载身份模式,无法为您创建密钥。

您的代码应检查从未出现的密钥,并警告用户遵循您提供的回退说明。

要通过Google Cloud Platform工作负载身份使用短期令牌对GCP进行身份验证,运营商必须提供以下信息:

AUDIENCE

由管理员在设置GCP工作负载身份时在GCP中创建,AUDIENCE值必须是以下格式的预格式化URL:

//iam.googleapis.com/projects/<project_number>/locations/global/workloadIdentityPools/<pool_id>/providers/<provider_id>
SERVICE_ACCOUNT_EMAIL

SERVICE_ACCOUNT_EMAIL值是在运营商操作期间被模拟的GCP服务帐户电子邮件,例如:

<service_account_name>@<project_id>.iam.gserviceaccount.com

Web控制台中的**安装运营商**页面允许集群管理员在安装时提供此信息。然后,此信息将作为运营商Pod上的环境变量传播到Subscription对象。

启用运营商以支持基于CCO的工作流和GCP工作负载身份

作为将项目设计为在Operator Lifecycle Manager (OLM)上运行的运营商作者,您可以通过自定义项目以支持云凭据运营商(CCO),从而使您的运营商能够对OpenShift Container Platform集群上的Google Cloud Platform工作负载身份进行身份验证。

使用此方法,运营商负责并需要RBAC权限才能创建CredentialsRequest对象并读取生成的Secret对象。

默认情况下,与 Operator 部署相关的 Pod 会挂载一个serviceAccountToken卷,以便可以在生成的Secret对象中引用服务帐户令牌。

前提条件
  • OpenShift Container Platform 4.17 或更高版本

  • 集群处于**GCP 工作负载身份/联合身份**模式

  • 基于 OLM 的 Operator 项目

步骤
  1. 更新您的 Operator 项目的ClusterServiceVersion (CSV) 对象

    1. 确保 CSV 中的 Operator 部署具有以下volumeMountsvolumes字段,以便 Operator 可以使用 Web 身份承担角色

      volumeMountsvolumes字段示例
      # ...
            volumeMounts:
      
            - name: bound-sa-token
              mountPath: /var/run/secrets/openshift/serviceaccount
              readOnly: true
            volumes:
               # This service account token can be used to provide identity outside the cluster.
               - name: bound-sa-token
                 projected:
                   sources:
                   - serviceAccountToken:
                     path: token
                     audience: openshift
    2. 确保您的 Operator 具有创建CredentialsRequests对象的 RBAC 权限

      clusterPermissions列表示例
      # ...
      install:
        spec:
          clusterPermissions:
          - rules:
            - apiGroups:
              - "cloudcredential.openshift.io"
              resources:
              - credentialsrequests
              verbs:
              - create
              - delete
              - get
              - list
              - patch
              - update
              - watch
    3. 添加以下注释以声明支持此基于 CCO 的工作流与 GCP 工作负载身份的方法

      # ...
      metadata:
       annotations:
         features.operators.openshift.io/token-auth-gcp: "true"
  2. 更新您的 Operator 项目代码

    1. 从订阅配置在 Pod 上设置的环境变量中获取audienceserviceAccountEmail

       // Get ENV var
         audience := os.Getenv("AUDIENCE")
         serviceAccountEmail := os.Getenv("SERVICE_ACCOUNT_EMAIL")
         gcpIdentityTokenFile := "/var/run/secrets/openshift/serviceaccount/token"
    2. 确保您已准备好一个CredentialsRequest对象,可以进行修补和应用。

      目前不支持向 Operator 包中添加CredentialsRequest对象。

    3. 在 Operator 初始化期间,将 GCP 工作负载身份变量添加到凭据请求并应用它

      Operator 初始化期间应用CredentialsRequest对象的示例
      // apply CredentialsRequest on install
         credReqTemplate.Spec.GCPProviderSpec.Audience = audience
         credReqTemplate.Spec.GCPProviderSpec.ServiceAccountEmail = serviceAccountEmail
         credReqTemplate.CloudTokenPath = gcpIdentityTokenFile
      
      
         c := mgr.GetClient()
         if err := c.Create(context.TODO(), credReq); err != nil {
             if !errors.IsAlreadyExists(err) {
                 setupLog.Error(err, "unable to create CredRequest")
                 os.Exit(1)
             }
         }
    4. 确保您的 Operator 可以等待 CCO 中显示Secret对象,如下例所示,该例与您在 Operator 中协调的其他项目一起调用

      等待Secret对象的示例
      // WaitForSecret is a function that takes a Kubernetes client, a namespace, and a v1 "k8s.io/api/core/v1" name as arguments
      // It waits until the secret object with the given name exists in the given namespace
      // It returns the secret object or an error if the timeout is exceeded
      func WaitForSecret(client kubernetes.Interface, namespace, name string) (*v1.Secret, error) {
        // set a timeout of 10 minutes
        timeout := time.After(10 * time.Minute) (1)
      
        // set a polling interval of 10 seconds
        ticker := time.NewTicker(10 * time.Second)
      
        // loop until the timeout or the secret is found
        for {
           select {
           case <-timeout:
              // timeout is exceeded, return an error
              return nil, fmt.Errorf("timed out waiting for secret %s in namespace %s", name, namespace)
      // add to this error with a pointer to instructions for following a manual path to a Secret that will work
           case <-ticker.C:
              // polling interval is reached, try to get the secret
              secret, err := client.CoreV1().Secrets(namespace).Get(context.Background(), name, metav1.GetOptions{})
              if err != nil {
                 if errors.IsNotFound(err) {
                    // secret does not exist yet, continue waiting
                    continue
                 } else {
                    // some other error occurred, return it
                    return nil, err
                 }
              } else {
                 // secret is found, return it
                 return secret, nil
              }
           }
        }
      }
      1 timeout值基于对 CCO 检测添加的CredentialsRequest对象并生成Secret对象的速度的估计。您可能需要考虑缩短时间或为集群管理员创建自定义反馈,这些管理员可能想知道为什么 Operator 尚未访问云资源。
    5. 读取 secret 中的service_account.json字段,并使用它来认证您的 GCP 客户端

      service_account_json := secret.StringData["service_account.json"]