Add new kube_cmd_versions table and API endpoint

In the DB, a new "kube_cmd_versions" table will be created,
aside from the boilerplate stuff, the important columns
will be "kubeadm_version" and "kubelet_version".
We want to default these to the highest-available
validated version of kubernetes in the load at the time.
There will be only 1 row here and the default will be created
in the DB migration script.

In the API a new endpoint is created '/v1/kube_cmd_versions',
and with this we can get and modify the kubeadm/kubelet version.

Story: 2008972
Task: 42895
Change-Id: Ic848ea777fecab5bda7488df225c2c3e924c2fd5
Signed-off-by: Mihnea Saracin <Mihnea.Saracin@windriver.com>
This commit is contained in:
Mihnea Saracin 2021-07-26 18:12:28 +03:00
parent 63798939c1
commit ca990c5a0d
12 changed files with 324 additions and 0 deletions

View File

@ -55,6 +55,7 @@ from cgtsclient.v1 import istor
from cgtsclient.v1 import isystem
from cgtsclient.v1 import iuser
from cgtsclient.v1 import kube_cluster
from cgtsclient.v1 import kube_cmd_version
from cgtsclient.v1 import kube_host_upgrade
from cgtsclient.v1 import kube_upgrade
from cgtsclient.v1 import kube_version
@ -167,6 +168,7 @@ class Client(http.HTTPClient):
self.host_fs = host_fs.HostFsManager(self)
self.kube_cluster = kube_cluster.KubeClusterManager(self)
self.kube_version = kube_version.KubeVersionManager(self)
self.kube_cmd_version = kube_cmd_version.KubeCmdVersionManager(self)
self.kube_upgrade = kube_upgrade.KubeUpgradeManager(self)
self.kube_host_upgrade = kube_host_upgrade.KubeHostUpgradeManager(self)
self.device_image = device_image.DeviceImageManager(self)

View File

@ -0,0 +1,29 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from cgtsclient.common import base
class KubeCmdVersion(base.Resource):
def __repr__(self):
return "<kube_cmd_version %s>" % self._info
class KubeCmdVersionManager(base.Manager):
resource_class = KubeCmdVersion
@staticmethod
def _path():
return '/v1/kube_cmd_versions'
def get(self):
try:
return self._list(self._path())[0]
except IndexError:
return None
def update(self, patch):
return self._update(self._path(), patch)

View File

@ -46,6 +46,7 @@ from sysinv.api.controllers.v1 import kube_host_upgrade
from sysinv.api.controllers.v1 import kube_rootca_update
from sysinv.api.controllers.v1 import kube_upgrade
from sysinv.api.controllers.v1 import kube_version
from sysinv.api.controllers.v1 import kube_cmd_version
from sysinv.api.controllers.v1 import label
from sysinv.api.controllers.v1 import interface
from sysinv.api.controllers.v1 import interface_network
@ -260,6 +261,9 @@ class V1(base.APIBase):
kube_versions = [link.Link]
"Links to the kube_version resource"
kube_cmd_versions = [link.Link]
"Links to the kube_cmd_version resource"
kube_upgrade = [link.Link]
"Links to the kube_upgrade resource"
@ -815,6 +819,13 @@ class V1(base.APIBase):
'kube_versions', '',
bookmark=True)]
v1.kube_cmd_versions = [link.Link.make_link('self', pecan.request.host_url,
'kube_cmd_versions', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'kube_cmd_versions', '',
bookmark=True)]
v1.kube_upgrade = [link.Link.make_link('self', pecan.request.host_url,
'kube_upgrade', ''),
link.Link.make_link('bookmark',
@ -935,6 +946,7 @@ class Controller(rest.RestController):
host_fs = host_fs.HostFsController()
kube_clusters = kube_cluster.KubeClusterController()
kube_versions = kube_version.KubeVersionController()
kube_cmd_versions = kube_cmd_version.KubeCmdVersionController()
kube_upgrade = kube_upgrade.KubeUpgradeController()
kube_rootca_update = kube_rootca_update.KubeRootCAUpdateController()
kube_host_upgrades = kube_host_upgrade.KubeHostUpgradeController()

View File

@ -0,0 +1,88 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import jsonpatch
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from oslo_log import log
from sysinv._i18n import _
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv import objects
LOG = log.getLogger(__name__)
LOCK_NAME = 'KubeCmdVersionController'
class KubeCmdVersionPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/kubeadm_version', '/kubelet_version']
class KubeCmdVersion(base.APIBase):
"""API representation of a k8s cmd version."""
kubeadm_version = wtypes.text
"Kubeadm version for this entry"
kubelet_version = wtypes.text
"Kubelet version for this entry"
def __init__(self, **kwargs):
self.fields = objects.kube_cmd_version.fields
for k in self.fields:
if not hasattr(self, k):
continue
setattr(self, k, kwargs.get(k, wtypes.Unset))
@classmethod
def convert_with_links(cls, rpc_kube_cmd_version):
kube_version = KubeCmdVersion(**rpc_kube_cmd_version.as_dict())
return kube_version
class KubeCmdVersionController(rest.RestController):
"""REST controller for Kubernetes Cmd Versions."""
@wsme_pecan.wsexpose(KubeCmdVersion)
def get(self):
"""Get the kube cmd version object"""
kube_cmd_version = objects.kube_cmd_version.get(pecan.request.context)
return KubeCmdVersion.convert_with_links(kube_cmd_version)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(KubeCmdVersion, body=[KubeCmdVersionPatchType])
def patch(self, patch):
"""Modify the kube cmd version object"""
try:
utils.validate_patch(patch)
patch_obj = jsonpatch.JsonPatch(patch)
kube_cmd_version = objects.kube_cmd_version.get(pecan.request.context)
kube_cmd_version_patched = KubeCmdVersion(**jsonpatch.apply_patch(
kube_cmd_version.as_dict(),
patch_obj))
# Update only the fields that have changed
for field in objects.kube_cmd_version.fields:
if kube_cmd_version[field] != getattr(kube_cmd_version_patched, field):
kube_cmd_version[field] = getattr(kube_cmd_version_patched, field)
kube_cmd_version.save()
except utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
except Exception as e:
LOG.exception(e)
raise wsme.exc.ClientSideError(_(
"Unable modify the KubeCmdVersion object."))
return KubeCmdVersion.convert_with_links(kube_cmd_version)

View File

@ -110,6 +110,9 @@ KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA = 'updating-host-trustNewCA'
KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA = 'updated-host-trustNewCA'
KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA_FAILED = 'updating-host-trustNewCA-failed'
# Kubeadm and Kubelet default versions
KUBERNETES_DEFAULT_VERSION = '1.18.1'
# Kubernetes constants
MANIFEST_APPLY_TIMEOUT = 60 * 15
MANIFEST_APPLY_INTERVAL = 10

View File

@ -4691,3 +4691,14 @@ class Connection(object):
:param rootca_update_id: id of the kubernetes rootca update entry to be deleted from database.
"""
@abc.abstractmethod
def kube_cmd_version_get(self):
""" Get the kubernetes cmd version entry"""
@abc.abstractmethod
def kube_cmd_version_update(self, values):
""" Update the kubernetes cmd version entry.
:param values: a dictionary with the respective fields and values to be updated in the db entry.
"""

View File

@ -8941,3 +8941,20 @@ class Connection(api.Connection):
except NoResultFound:
raise exception.KubeRootCAUpdateNotFound(rootca_update_id=rootca_update_id)
query.delete()
@objects.objectify(objects.kube_cmd_version)
def kube_cmd_version_get(self):
query = model_query(models.KubeCmdVersions)
try:
return query.one()
except NoResultFound:
raise exception.NotFound()
@objects.objectify(objects.kube_cmd_version)
def kube_cmd_version_update(self, values):
with _session_for_write() as session:
query = model_query(models.KubeCmdVersions, session=session)
count = query.update(values)
if count != 1:
raise exception.NotFound()
return query.one()

View File

@ -0,0 +1,62 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from migrate.changeset import UniqueConstraint
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sysinv.common import kubernetes
ENGINE = 'InnoDB'
CHARSET = 'utf8'
def _insert_default_kube_cmd_version(kube_cmd_versions):
kube_cmd_versions_insert = kube_cmd_versions.insert()
values = {
'kubeadm_version': kubernetes.KUBERNETES_DEFAULT_VERSION,
'kubelet_version': kubernetes.KUBERNETES_DEFAULT_VERSION
}
kube_cmd_versions_insert.execute(values)
def upgrade(migrate_engine):
"""
This database upgrade creates a new kube_cmd_versions table
"""
meta = MetaData()
meta.bind = migrate_engine
# Define and create the kube_cmd_versions table.
kube_cmd_versions = Table(
'kube_cmd_versions',
meta,
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('id', Integer, primary_key=True,
unique=True, nullable=False),
Column('kubeadm_version', String(255), nullable=False),
Column('kubelet_version', String(255), nullable=False),
UniqueConstraint('kubeadm_version', 'kubelet_version',
name='u_kubeadm_version_kubelet_version'),
mysql_engine=ENGINE,
mysql_charset=CHARSET,
)
kube_cmd_versions.create()
_insert_default_kube_cmd_version(kube_cmd_versions)
def downgrade(migrate_engine):
# Downgrade is unsupported in this release.
raise NotImplementedError('SysInv database downgrade is unsupported.')

View File

@ -1958,3 +1958,13 @@ class KubeRootCAHostUpdate(Base):
reserved_3 = Column(String(255))
host = relationship("ihost", lazy="joined", join_depth=1)
class KubeCmdVersions(Base):
__tablename__ = 'kube_cmd_versions'
id = Column(Integer, primary_key=True)
kubeadm_version = Column(String(255), nullable=False)
kubelet_version = Column(String(255), nullable=False)
UniqueConstraint('kubeadm_version', 'kubelet_version',
name='u_kubeadm_version_kubelet_version')

View File

@ -45,6 +45,7 @@ from sysinv.objects import kube_app_releases
from sysinv.objects import kube_host_upgrade
from sysinv.objects import kube_upgrade
from sysinv.objects import kube_version
from sysinv.objects import kube_cmd_version
from sysinv.objects import interface
from sysinv.objects import interface_ae
from sysinv.objects import interface_ethernet
@ -199,6 +200,7 @@ kube_app_releases = kube_app_releases.KubeAppReleases
kube_host_upgrade = kube_host_upgrade.KubeHostUpgrade
kube_upgrade = kube_upgrade.KubeUpgrade
kube_version = kube_version.KubeVersion
kube_cmd_version = kube_cmd_version.KubeCmdVersion
datanetwork = datanetwork.DataNetwork
host_fs = host_fs.HostFS
device_image = device_image.DeviceImage
@ -278,6 +280,7 @@ __all__ = ("system",
"kube_host_upgrade",
"kube_upgrade",
"kube_version",
"kube_cmd_version",
"datanetwork",
"interface_network",
"host_fs",

View File

@ -0,0 +1,26 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from sysinv.db import api as db_api
from sysinv.objects import base
from sysinv.objects import utils
class KubeCmdVersion(base.SysinvObject):
dbapi = db_api.get_instance()
fields = {
'kubeadm_version': utils.str_or_none,
'kubelet_version': utils.str_or_none,
}
@base.remotable_classmethod
def get(cls, context):
return cls.dbapi.kube_cmd_version_get()
def save_changes(self, context, updates):
self.dbapi.kube_cmd_version_update(updates)

View File

@ -0,0 +1,61 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from six.moves import http_client
from sysinv.tests.api import base
from sysinv.common import kubernetes
class TestKubeCmdVersion(base.FunctionalTest):
API_HEADERS = {'User-Agent': 'sysinv-test'}
API_PREFIX = '/kube_cmd_versions'
expected_api_fields = ['kubeadm_version',
'kubelet_version']
def setUp(self):
super(TestKubeCmdVersion, self).setUp()
def assert_fields(self, api_object):
# Verify that expected attributes are returned
for field in self.expected_api_fields:
self.assertIn(field, api_object)
def test_show_kube_cmd_version_success(self):
response = self.get_json(self.API_PREFIX,
headers=self.API_HEADERS)
self.assert_fields(response)
self.assertEqual(response['kubeadm_version'],
kubernetes.KUBERNETES_DEFAULT_VERSION)
self.assertEqual(response['kubelet_version'],
kubernetes.KUBERNETES_DEFAULT_VERSION)
def test_patch_kube_cmd_version_success(self):
values = {
'kubeadm_version': '1.5.1',
'kubelet_version': '1.5.2'
}
response = self.patch_dict_json(self.API_PREFIX,
headers=self.API_HEADERS,
**values)
self.assertEqual(http_client.OK, response.status_int)
self.assert_fields(response.json)
for k in values:
self.assertEqual(values[k], response.json[k])
get_response = self.get_json(self.API_PREFIX,
headers=self.API_HEADERS)
for k in values:
self.assertEqual(values[k], get_response[k])
def test_patch_kube_cmd_version_failure(self):
wrong_value = {
'kube_version': '1.5.1'
}
response = self.patch_dict_json(self.API_PREFIX,
headers=self.API_HEADERS,
expect_errors=True,
**wrong_value)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertTrue(response.json.get('error_message'))