Skip to content

Commit

Permalink
Deprecate Kubernetes client API version v1alpha1
Browse files Browse the repository at this point in the history
Kubernetes has deprecated v1alpha1, v1beta1 has been available since Kubernetes
v1.11 (kubernetes/kubernetes#64482), and EKS currently supports Kubernetes
versions v1.16 through v1.21. This is a breaking change for clients running
versions v1.10 and older, which haven't been supported by EKS since September
2019.

"aws eks get-token" now respects the KUBERNETES_EXEC_INFO environment
variable and conservatively falls back to v1alpha1, which is supported
by Kubernetes versions 1.10 through 1.22 (released upstream August 2021, to be
released by EKS in Q4 2021). It also now supports "v1beta1" and "v1".

"aws eks update-kubeconfig" now writes "v1beta1" in the kubeconfig which
will be supported by Kubernetes until 1.29 (aproximately December 2023).
At or around that date, we can change the default version written to
kubeconfigs to "v1"

Signed-off-by: Micah Hausler <[email protected]>
  • Loading branch information
micahhausler authored and justindho committed May 5, 2022
1 parent e564532 commit 1a6b498
Show file tree
Hide file tree
Showing 26 changed files with 272 additions and 41 deletions.
70 changes: 69 additions & 1 deletion awscli/customizations/eks/get_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import base64
import botocore
import json
import os
import sys

from datetime import datetime, timedelta
from botocore.signers import RequestSigner
Expand Down Expand Up @@ -74,7 +76,7 @@ def _run_main(self, parsed_args, parsed_globals):

full_object = {
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"apiVersion": self.discover_api_version(),
"spec": {},
"status": {
"expirationTimestamp": token_expiration,
Expand All @@ -86,6 +88,72 @@ def _run_main(self, parsed_args, parsed_globals):
uni_print('\n')
return 0

def discover_api_version(self):
"""
Parses the KUBERNETES_EXEC_INFO environment variable and returns the API
version. If the environment variable is empty, malformed, or invalid,
return the v1alpha1 response and print an message to stderr.
If the v1alpha1 API is specified explicitly, a message is printed to
stderr with instructions to update.
:return: The client authentication API version
:rtype: string
"""
alpha_api = "client.authentication.k8s.io/v1alpha1"
beta_api = "client.authentication.k8s.io/v1beta1"
v1_api = "client.authentication.k8s.io/v1"
# At the time Kubernetes v1.29 is released upstream (aprox Dec 2023),
# "v1beta1" will be removed. At or around that time, EKS will likely
# support v1.22 through v1.28, in which client API version "v1beta1"
# will be supported by all EKS versions.
fallback_api_version = alpha_api

error_prefixes = {
"error": "Error parsing",
"empty": "Empty",
}

error_msg_tpl = ("{0} KUBERNETES_EXEC_INFO, defaulting "
"to {1}. This is likely a bug in your Kubernetes "
"client. Please update your Kubernetes client.")
unrecognized_msg = ("Unrecognized API version in KUBERNETES_EXEC_INFO, defaulting "
"to {0}. This is likely due to an outdated AWS CLI."
" Please update your AWS CLI.".format(fallback_api_version))
deprecation_msg_tpl = ("Kubeconfig user entry is using deprecated API "
"version {0}. Run 'aws eks update-kubeconfig' to update")

exec_info_raw = os.environ.get("KUBERNETES_EXEC_INFO", "")
if len(exec_info_raw) == 0:
# All kube clients should be setting this, we'll return the fallback and write an error
sys.stderr.write(error_msg_tpl.format(error_prefixes["empty"], fallback_api_version))
sys.stderr.write("\n")
sys.stderr.flush()
return fallback_api_version
try:
exec_info = json.loads(exec_info_raw)
except json.JSONDecodeError as e:
# The environment variable was malformed
sys.stderr.write(error_msg_tpl.format(error_prefixes["error"], fallback_api_version))
sys.stderr.write("\n")
sys.stderr.flush()
return fallback_api_version

api_version_raw = exec_info.get("apiVersion")
if api_version_raw == v1_api:
return v1_api
if api_version_raw == beta_api:
return beta_api
if api_version_raw == alpha_api:
sys.stderr.write(deprecation_msg_tpl.format(alpha_api))
sys.stderr.write("\n")
sys.stderr.flush()
return alpha_api

sys.stderr.write(unrecognized_msg)
sys.stderr.write("\n")
sys.stderr.flush()
return fallback_api_version

class TokenGenerator(object):
def __init__(self, sts_client):
Expand Down
9 changes: 4 additions & 5 deletions awscli/customizations/eks/update_kubeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@

DEFAULT_PATH = os.path.expanduser("~/.kube/config")

# Use the endpoint for kubernetes 1.10
# To get the most recent endpoint we will need to
# Do a check on the cluster's version number
API_VERSION = "client.authentication.k8s.io/v1alpha1"
# At the time EKS no longer supports Kubernetes v1.21 (probably ~Dec 2023),
# this can be safely changed to default to writing "v1"
API_VERSION = "client.authentication.k8s.io/v1beta1"

class UpdateKubeconfigCommand(BasicCommand):
NAME = 'update-kubeconfig'
Expand Down Expand Up @@ -306,7 +305,7 @@ def get_user_entry(self):
"--cluster-name",
self._cluster_name,
]),
("command", "aws")
("command", "aws"),
]))
]))
])
Expand Down
2 changes: 1 addition & 1 deletion awscli/examples/eks/get-token.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Output::

{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {},
"status": {
"expirationTimestamp": "2019-08-14T18:44:27Z",
Expand Down
103 changes: 103 additions & 0 deletions tests/functional/eks/test_get_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import base64
from datetime import datetime
import json
import os

from awscli.testutils import mock
from awscli.testutils import BaseAWSCommandParamsTest
Expand Down Expand Up @@ -143,3 +144,105 @@ def test_url_different_partition(self):
expected_endpoint='sts.cn-north-1.amazonaws.com.cn',
expected_signing_region='cn-north-1'
)

def test_api_version_discovery_deprecated(self):
os.environ["KUBERNETES_EXEC_INFO"] = '{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1alpha1","spec":{"interactive":true}}'
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1alpha1",
)

self.assertEqual(
stderr,
("Kubeconfig user entry is using deprecated API "
"version client.authentication.k8s.io/v1alpha1. Run 'aws eks update-kubeconfig' to update\n")
)

del os.environ["KUBERNETES_EXEC_INFO"]

def test_api_version_discovery_malformed(self):
os.environ["KUBERNETES_EXEC_INFO"] = '{{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1alpha1","spec":{"interactive":true}}'
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1alpha1",
)

self.assertEqual(
stderr,
("Error parsing KUBERNETES_EXEC_INFO, defaulting to client.authentication.k8s.io/v1alpha1. "
"This is likely a bug in your Kubernetes client. Please update your Kubernetes client.\n")
)

del os.environ["KUBERNETES_EXEC_INFO"]

def test_api_version_discovery_empty(self):
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1alpha1",
)

self.assertEqual(
stderr,
("Empty KUBERNETES_EXEC_INFO, defaulting to client.authentication.k8s.io/v1alpha1. "
"This is likely a bug in your Kubernetes client. Please update your Kubernetes client.\n")
)
def test_api_version_discovery_v1(self):
os.environ["KUBERNETES_EXEC_INFO"] = '{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1","spec":{"interactive":true}}'
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1",
)

self.assertEqual(stderr, "")

del os.environ["KUBERNETES_EXEC_INFO"]

def test_api_version_discovery_v1beta1(self):
os.environ["KUBERNETES_EXEC_INFO"] = '{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":true}}'
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1beta1",
)

self.assertEqual(stderr, "")

del os.environ["KUBERNETES_EXEC_INFO"]

def test_api_version_discovery_unknown(self):
os.environ["KUBERNETES_EXEC_INFO"] = '{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v2","spec":{"interactive":true}}'
cmd = 'eks get-token --cluster-name %s' % self.cluster_name
stdout, stderr, _ = self.run_cmd(cmd)
response = json.loads(stdout)

self.assertEqual(
response["apiVersion"],
"client.authentication.k8s.io/v1alpha1",
)

self.assertEqual(
stderr,
("Unrecognized API version in KUBERNETES_EXEC_INFO, defaulting to client.authentication.k8s.io/v1alpha1. "
"This is likely due to an outdated AWS CLI. Please update your AWS CLI.\n")
)

del os.environ["KUBERNETES_EXEC_INFO"]
10 changes: 5 additions & 5 deletions tests/functional/eks/test_kubeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def _clone_config(self, config):
"""
old_path = os.path.abspath(get_testdata(config))
new_path = os.path.join(self._temp_directory, config)
shutil.copy2(old_path,
shutil.copy2(old_path,
new_path)
return new_path

Expand All @@ -119,25 +119,25 @@ def test_load_simple(self):
])
loaded_config = self._loader.load_kubeconfig(simple_path)
self.assertEqual(loaded_config.content, content)
self._validator.validate_config.called_with(Kubeconfig(simple_path,
self._validator.validate_config.called_with(Kubeconfig(simple_path,
content))

def test_load_noexist(self):
no_exist_path = os.path.join(self._temp_directory,
"this_does_not_exist")
loaded_config = self._loader.load_kubeconfig(no_exist_path)
self.assertEqual(loaded_config.content,
self.assertEqual(loaded_config.content,
_get_new_kubeconfig_content())
self._validator.validate_config.called_with(
Kubeconfig(no_exist_path, _get_new_kubeconfig_content()))

def test_load_empty(self):
empty_path = self._clone_config("valid_empty_existing")
loaded_config = self._loader.load_kubeconfig(empty_path)
self.assertEqual(loaded_config.content,
self.assertEqual(loaded_config.content,
_get_new_kubeconfig_content())
self._validator.validate_config.called_with(
Kubeconfig(empty_path,
Kubeconfig(empty_path,
_get_new_kubeconfig_content()))

def test_load_directory(self):
Expand Down
23 changes: 15 additions & 8 deletions tests/functional/eks/test_update_kubeconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ def setUp(self):
self.create_client_patch = mock.patch(
'botocore.session.Session.create_client'
)

self.mock_create_client = self.create_client_patch.start()
self.session = get_session()

self.client = mock.Mock()
self.client.describe_cluster.return_value = describe_cluster_response()
self.mock_create_client.return_value = self.client

self.command = UpdateKubeconfigCommand(self.session)
self.maxDiff = None

Expand Down Expand Up @@ -102,7 +102,7 @@ def initialize_tempfiles(self, files):
self.addCleanup(shutil.rmtree, self._temp_directory)
if files is not None:
for file in files:
shutil.copy2(get_testdata(file),
shutil.copy2(get_testdata(file),
self._get_temp_config(file))
return self._temp_directory

Expand All @@ -116,7 +116,7 @@ def build_temp_environment_variable(self, configs):
to put in the environment variable
:type configs: list
"""
return build_environment([self._get_temp_config(config)
return build_environment([self._get_temp_config(config)
for config in configs])

def assert_config_state(self, config_name, correct_output_name):
Expand All @@ -125,7 +125,7 @@ def assert_config_state(self, config_name, correct_output_name):
as the testdata named correct_output_name.
Should be called after initialize_tempfiles.
:param config_name: The filename (not the path) of the tempfile
:param config_name: The filename (not the path) of the tempfile
to compare
:type config_name: str
Expand Down Expand Up @@ -189,7 +189,7 @@ def assert_cmd(self, configs, passed_config,
verbose=False):
"""
Run update-kubeconfig in a temp directory,
This directory will have copies of all testdata files whose names
This directory will have copies of all testdata files whose names
are listed in configs.
The KUBECONFIG environment variable will be set to contain the configs
listed in env_variable_configs (regardless of whether they exist).
Expand Down Expand Up @@ -384,7 +384,7 @@ def test_update_existing(self):
configs = ["valid_old_data"]
passed = "valid_old_data"
environment = []

self.assert_cmd(configs, passed, environment)
self.assert_config_state("valid_old_data", "output_combined")

Expand All @@ -394,7 +394,7 @@ def test_update_existing_environment(self):
environment = ["valid_old_data",
"output_combined",
"output_single"]

self.assert_cmd(configs, passed, environment)
self.assert_config_state("valid_old_data", "output_combined")

Expand All @@ -415,3 +415,10 @@ def test_kubeconfig_order(self):
self.assert_cmd(configs, passed, environment)
self.assert_config_state("valid_changed_ordering", "output_combined_changed_ordering")

def test_update_old_api_version(self):
configs = ["valid_old_api_version"]
passed = "valid_old_api_version"
environment = []

self.assert_cmd(configs, passed, environment)
self.assert_config_state("valid_old_api_version", "valid_old_api_version_updated")
2 changes: 1 addition & 1 deletion tests/functional/eks/testdata/invalid_string_cluster_entry
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ users:
- name: arn:aws:eks:us-west-2:111222333444:cluster/Existing
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
apiVersion: client.authentication.k8s.io/v1beta1
args:
- --region
- us-west-2
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/eks/testdata/invalid_string_clusters
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ users:
- name: arn:aws:eks:us-west-2:111222333444:cluster/Existing
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
apiVersion: client.authentication.k8s.io/v1beta1
args:
- --region
- us-west-2
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/eks/testdata/invalid_string_context_entry
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ users:
- name: arn:aws:eks:us-west-2:111222333444:cluster/Existing
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
apiVersion: client.authentication.k8s.io/v1beta1
args:
- --region
- us-west-2
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/eks/testdata/invalid_string_contexts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ users:
- name: arn:aws:eks:us-west-2:111222333444:cluster/Existing
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
apiVersion: client.authentication.k8s.io/v1beta1
args:
- --region
- us-west-2
Expand Down
Loading

0 comments on commit 1a6b498

Please sign in to comment.