1313import base64
1414import botocore
1515import json
16+ import os
17+ import sys
1618
1719from datetime import datetime , timedelta
1820from botocore .signers import RequestSigner
2628AUTH_API_VERSION = "2011-06-15"
2729AUTH_SIGNING_VERSION = "v4"
2830
31+ ALPHA_API = "client.authentication.k8s.io/v1alpha1"
32+ BETA_API = "client.authentication.k8s.io/v1beta1"
33+ V1_API = "client.authentication.k8s.io/v1"
34+
35+ FULLY_SUPPORTED_API_VERSIONS = [
36+ V1_API ,
37+ BETA_API ,
38+ ]
39+ DEPRECATED_API_VERSIONS = [
40+ ALPHA_API ,
41+ ]
42+
43+ ERROR_MSG_TPL = (
44+ "{0} KUBERNETES_EXEC_INFO, defaulting to {1}. This is likely a "
45+ "bug in your Kubernetes client. Please update your Kubernetes "
46+ "client."
47+ )
48+ UNRECOGNIZED_MSG_TPL = (
49+ "Unrecognized API version in KUBERNETES_EXEC_INFO, defaulting to "
50+ "{0}. This is likely due to an outdated AWS "
51+ "CLI. Please update your AWS CLI."
52+ )
53+ DEPRECATION_MSG_TPL = (
54+ "Kubeconfig user entry is using deprecated API version {0}. Run "
55+ "'aws eks update-kubeconfig' to update."
56+ )
57+
2958# Presigned url timeout in seconds
3059URL_TIMEOUT = 60
3160
3968class GetTokenCommand (BasicCommand ):
4069 NAME = 'get-token'
4170
42- DESCRIPTION = ("Get a token for authentication with an Amazon EKS cluster. "
43- "This can be used as an alternative to the "
44- "aws-iam-authenticator." )
71+ DESCRIPTION = (
72+ "Get a token for authentication with an Amazon EKS cluster. "
73+ "This can be used as an alternative to the "
74+ "aws-iam-authenticator."
75+ )
4576
4677 ARG_TABLE = [
4778 {
4879 'name' : 'cluster-name' ,
49- 'help_text' : ("Specify the name of the Amazon EKS cluster to create a token for." ),
50- 'required' : True
80+ 'help_text' : (
81+ "Specify the name of the Amazon EKS cluster to create a token for."
82+ ),
83+ 'required' : True ,
5184 },
5285 {
5386 'name' : 'role-arn' ,
54- 'help_text' : ("Assume this role for credentials when signing the token." ),
55- 'required' : False
56- }
87+ 'help_text' : (
88+ "Assume this role for credentials when signing the token."
89+ ),
90+ 'required' : False ,
91+ },
5792 ]
5893
5994 def get_expiration_time (self ):
60- token_expiration = datetime .utcnow () + timedelta (minutes = TOKEN_EXPIRATION_MINS )
95+ token_expiration = datetime .utcnow () + timedelta (
96+ minutes = TOKEN_EXPIRATION_MINS
97+ )
6198 return token_expiration .strftime ('%Y-%m-%dT%H:%M:%SZ' )
6299
63100 def _run_main (self , parsed_args , parsed_globals ):
64101 client_factory = STSClientFactory (self ._session )
65102 sts_client = client_factory .get_sts_client (
66- region_name = parsed_globals .region ,
67- role_arn = parsed_args . role_arn )
103+ region_name = parsed_globals .region , role_arn = parsed_args . role_arn
104+ )
68105 token = TokenGenerator (sts_client ).get_token (parsed_args .cluster_name )
69106
70107 # By default STS signs the url for 15 minutes so we are creating a
@@ -74,28 +111,94 @@ def _run_main(self, parsed_args, parsed_globals):
74111
75112 full_object = {
76113 "kind" : "ExecCredential" ,
77- "apiVersion" : "client.authentication.k8s.io/v1alpha1" ,
114+ "apiVersion" : self . discover_api_version () ,
78115 "spec" : {},
79116 "status" : {
80117 "expirationTimestamp" : token_expiration ,
81- "token" : token
82- }
118+ "token" : token ,
119+ },
83120 }
84121
85122 uni_print (json .dumps (full_object ))
86123 uni_print ('\n ' )
87124 return 0
88125
126+ def discover_api_version (self ):
127+ """
128+ Parses the KUBERNETES_EXEC_INFO environment variable and returns the
129+ API version. If the environment variable is empty, malformed, or
130+ invalid, return the v1alpha1 response and print a message to stderr.
131+
132+ If the v1alpha1 API is specified explicitly, a message is printed to
133+ stderr with instructions to update.
134+
135+ :return: The client authentication API version
136+ :rtype: string
137+ """
138+ # At the time Kubernetes v1.29 is released upstream (approx Dec 2023),
139+ # "v1beta1" will be removed. At or around that time, EKS will likely
140+ # support v1.22 through v1.28, in which client API version "v1beta1"
141+ # will be supported by all EKS versions.
142+ fallback_api_version = ALPHA_API
143+
144+ error_prefixes = {
145+ "error" : "Error parsing" ,
146+ "empty" : "Empty" ,
147+ }
148+
149+ exec_info_raw = os .environ .get ("KUBERNETES_EXEC_INFO" , "" )
150+ if not exec_info_raw :
151+ # All kube clients should be setting this. Otherewise, we'll return
152+ # the fallback and write an error.
153+ uni_print (
154+ ERROR_MSG_TPL .format (
155+ error_prefixes ["empty" ],
156+ fallback_api_version ,
157+ ),
158+ sys .stderr ,
159+ )
160+ uni_print ("\n " , sys .stderr )
161+ return fallback_api_version
162+ try :
163+ exec_info = json .loads (exec_info_raw )
164+ except json .JSONDecodeError :
165+ # The environment variable was malformed
166+ uni_print (
167+ ERROR_MSG_TPL .format (
168+ error_prefixes ["error" ],
169+ fallback_api_version ,
170+ ),
171+ sys .stderr ,
172+ )
173+ uni_print ("\n " , sys .stderr )
174+ return fallback_api_version
175+
176+ api_version_raw = exec_info .get ("apiVersion" )
177+ if api_version_raw in FULLY_SUPPORTED_API_VERSIONS :
178+ return api_version_raw
179+ elif api_version_raw in DEPRECATED_API_VERSIONS :
180+ uni_print (DEPRECATION_MSG_TPL .format (ALPHA_API ), sys .stderr )
181+ uni_print ("\n " , sys .stderr )
182+ return api_version_raw
183+ else :
184+ uni_print (
185+ UNRECOGNIZED_MSG_TPL .format (fallback_api_version ),
186+ sys .stderr ,
187+ )
188+ uni_print ("\n " , sys .stderr )
189+ return fallback_api_version
190+
89191
90192class TokenGenerator (object ):
91193 def __init__ (self , sts_client ):
92194 self ._sts_client = sts_client
93195
94196 def get_token (self , cluster_name ):
95- """ Generate a presigned url token to pass to kubectl. """
197+ """Generate a presigned url token to pass to kubectl."""
96198 url = self ._get_presigned_url (cluster_name )
97199 token = TOKEN_PREFIX + base64 .urlsafe_b64encode (
98- url .encode ('utf-8' )).decode ('utf-8' ).rstrip ('=' )
200+ url .encode ('utf-8' )
201+ ).decode ('utf-8' ).rstrip ('=' )
99202 return token
100203
101204 def _get_presigned_url (self , cluster_name ):
@@ -112,9 +215,7 @@ def __init__(self, session):
112215 self ._session = session
113216
114217 def get_sts_client (self , region_name = None , role_arn = None ):
115- client_kwargs = {
116- 'region_name' : region_name
117- }
218+ client_kwargs = {'region_name' : region_name }
118219 if role_arn is not None :
119220 creds = self ._get_role_credentials (region_name , role_arn )
120221 client_kwargs ['aws_access_key_id' ] = creds ['AccessKeyId' ]
@@ -127,18 +228,17 @@ def get_sts_client(self, region_name=None, role_arn=None):
127228 def _get_role_credentials (self , region_name , role_arn ):
128229 sts = self ._session .create_client ('sts' , region_name )
129230 return sts .assume_role (
130- RoleArn = role_arn ,
131- RoleSessionName = 'EKSGetTokenAuth'
231+ RoleArn = role_arn , RoleSessionName = 'EKSGetTokenAuth'
132232 )['Credentials' ]
133233
134234 def _register_cluster_name_handlers (self , sts_client ):
135235 sts_client .meta .events .register (
136236 'provide-client-params.sts.GetCallerIdentity' ,
137- self ._retrieve_cluster_name
237+ self ._retrieve_cluster_name ,
138238 )
139239 sts_client .meta .events .register (
140240 'before-sign.sts.GetCallerIdentity' ,
141- self ._inject_cluster_name_header
241+ self ._inject_cluster_name_header ,
142242 )
143243
144244 def _retrieve_cluster_name (self , params , context , ** kwargs ):
@@ -147,5 +247,6 @@ def _retrieve_cluster_name(self, params, context, **kwargs):
147247
148248 def _inject_cluster_name_header (self , request , ** kwargs ):
149249 if 'eks_cluster' in request .context :
150- request .headers [
151- CLUSTER_NAME_HEADER ] = request .context ['eks_cluster' ]
250+ request .headers [CLUSTER_NAME_HEADER ] = request .context [
251+ 'eks_cluster'
252+ ]
0 commit comments