1
0
mirror of https://github.com/openshift/openshift-docs.git synced 2026-02-05 12:46:18 +01:00
Files
openshift-docs/modules/osdk-cco-aws-sts-enabling.adoc
2026-01-27 21:08:23 +00:00

323 lines
9.9 KiB
Plaintext

// Module included in the following assemblies:
//
// * operators/operator_sdk/osdk-token-auth.adoc
// * hosted_control_planes/hcp-authentication-authorization.adoc
:_mod-docs-content-type: PROCEDURE
[id="osdk-cco-aws-sts-enabling_{context}"]
= Enabling Operators to support CCO-based workflows with AWS STS
As an Operator author designing your project to run on Operator Lifecycle Manager (OLM), you can enable your Operator to authenticate against AWS on STS-enabled {product-title} clusters by customizing your project to support the Cloud Credential Operator (CCO).
With this method, the Operator is responsible for and requires RBAC permissions for creating the `CredentialsRequest` object and reading the resulting `Secret` object.
[NOTE]
====
By default, pods related to the Operator deployment mount a `serviceAccountToken` volume so that the service account token can be referenced in the resulting `Secret` object.
====
.Prerequisites
* {product-title} 4.14 or later
* Cluster in STS mode
* OLM-based Operator project
.Procedure
. Update your Operator project's `ClusterServiceVersion` (CSV) object:
.. Ensure your Operator has RBAC permission to create `CredentialsRequests` objects:
+
.Example `clusterPermissions` list
[%collapsible]
====
[source,yaml]
----
# ...
install:
spec:
clusterPermissions:
- rules:
- apiGroups:
- "cloudcredential.openshift.io"
resources:
- credentialsrequests
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
----
====
.. Add the following annotation to claim support for this method of CCO-based workflow with AWS STS:
+
[source,yaml]
----
# ...
metadata:
annotations:
features.operators.openshift.io/token-auth-aws: "true"
----
. Update your Operator project code:
.. Get the role ARN from the environment variable set on the pod by the `Subscription` object. For example:
+
[source,go]
----
// Get ENV var
roleARN := os.Getenv("ROLEARN")
setupLog.Info("getting role ARN", "role ARN = ", roleARN)
webIdentityTokenPath := "/var/run/secrets/openshift/serviceaccount/token"
----
.. Ensure you have a `CredentialsRequest` object ready to be patched and applied. For example:
+
.Example `CredentialsRequest` object creation
[%collapsible]
====
[source,go]
----
import (
minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var in = minterv1.AWSProviderSpec{
StatementEntries: []minterv1.StatementEntry{
{
Action: []string{
"s3:*",
},
Effect: "Allow",
Resource: "arn:aws:s3:*:*:*",
},
},
STSIAMRoleARN: "<role_arn>",
}
var codec = minterv1.Codec
var ProviderSpec, _ = codec.EncodeProviderSpec(in.DeepCopyObject())
const (
name = "<credential_request_name>"
namespace = "<namespace_name>"
)
var CredentialsRequestTemplate = &minterv1.CredentialsRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "openshift-cloud-credential-operator",
},
Spec: minterv1.CredentialsRequestSpec{
ProviderSpec: ProviderSpec,
SecretRef: corev1.ObjectReference{
Name: "<secret_name>",
Namespace: namespace,
},
ServiceAccountNames: []string{
"<service_account_name>",
},
CloudTokenPath: "",
},
}
----
====
+
Alternatively, if you are starting from a `CredentialsRequest` object in YAML form (for example, as part of your Operator project code), you can handle it differently:
+
.Example `CredentialsRequest` object creation in YAML form
[%collapsible]
====
[source,go]
----
// CredentialsRequest is a struct that represents a request for credentials
type CredentialsRequest struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
} `yaml:"metadata"`
Spec struct {
SecretRef struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
} `yaml:"secretRef"`
ProviderSpec struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
StatementEntries []struct {
Effect string `yaml:"effect"`
Action []string `yaml:"action"`
Resource string `yaml:"resource"`
} `yaml:"statementEntries"`
STSIAMRoleARN string `yaml:"stsIAMRoleARN"`
} `yaml:"providerSpec"`
// added new field
CloudTokenPath string `yaml:"cloudTokenPath"`
} `yaml:"spec"`
}
// ConsumeCredsRequestAddingTokenInfo is a function that takes a YAML filename and two strings as arguments
// It unmarshals the YAML file to a CredentialsRequest object and adds the token information.
func ConsumeCredsRequestAddingTokenInfo(fileName, tokenString, tokenPath string) (*CredentialsRequest, error) {
// open a file containing YAML form of a CredentialsRequest
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer file.Close()
// create a new CredentialsRequest object
cr := &CredentialsRequest{}
// decode the yaml file to the object
decoder := yaml.NewDecoder(file)
err = decoder.Decode(cr)
if err != nil {
return nil, err
}
// assign the string to the existing field in the object
cr.Spec.CloudTokenPath = tokenPath
// return the modified object
return cr, nil
}
----
====
+
[NOTE]
====
Adding a `CredentialsRequest` object to the Operator bundle is not currently supported.
====
.. Add the role ARN and web identity token path to the credentials request and apply it during Operator initialization:
+
.Example applying `CredentialsRequest` object during Operator initialization
[%collapsible]
====
[source,go]
----
// apply CredentialsRequest on install
credReq := credreq.CredentialsRequestTemplate
credReq.Spec.CloudTokenPath = webIdentityTokenPath
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)
}
}
----
====
.. Ensure your Operator can wait for a `Secret` object to show up from the CCO, as shown in the following example, which is called along with the other items you are reconciling in your Operator:
+
.Example wait for `Secret` object
[%collapsible]
====
[source,go]
----
// 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 on STS
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> The `timeout` value is based on an estimate of how fast the CCO might detect an added `CredentialsRequest` object and generate a `Secret` object. You might consider lowering the time or creating custom feedback for cluster administrators that could be wondering why the Operator is not yet accessing the cloud resources.
====
.. Set up the AWS configuration by reading the secret created by the CCO from the credentials request and creating the AWS config file containing the data from that secret:
+
.Example AWS configuration creation
[%collapsible]
====
[source,go]
----
func SharedCredentialsFileFromSecret(secret *corev1.Secret) (string, error) {
var data []byte
switch {
case len(secret.Data["credentials"]) > 0:
data = secret.Data["credentials"]
default:
return "", errors.New("invalid secret for aws credentials")
}
f, err := ioutil.TempFile("", "aws-shared-credentials")
if err != nil {
return "", errors.Wrap(err, "failed to create file for shared credentials")
}
defer f.Close()
if _, err := f.Write(data); err != nil {
return "", errors.Wrapf(err, "failed to write credentials to %s", f.Name())
}
return f.Name(), nil
}
----
====
+
[IMPORTANT]
====
The secret is assumed to exist, but your Operator code should wait and retry when using this secret to give time to the CCO to create the secret.
Additionally, the wait period should eventually time out and warn users that the {product-title} cluster version, and therefore the CCO, might be an earlier version that does not support the `CredentialsRequest` object workflow with STS detection. In such cases, instruct users that they must add a secret by using another method.
====
.. Configure the AWS SDK session, for example:
+
.Example AWS SDK session configuration
[%collapsible]
====
[source,go]
----
sharedCredentialsFile, err := SharedCredentialsFileFromSecret(secret)
if err != nil {
// handle error
}
options := session.Options{
SharedConfigState: session.SharedConfigEnable,
SharedConfigFiles: []string{sharedCredentialsFile},
}
----
====