Skip to content

Commit c85b955

Browse files
committed
Add credential API
Implements the 'credentials' endpoint to Magnum API, initially supporting rotation of cluster credentials dictated by the underlying driver. Change-Id: Idaab3c0d1fdca4b15505a949182ceb1d987d3a7f Signed-off-by: Matthew Northcott <[email protected]>
1 parent 10e87db commit c85b955

File tree

13 files changed

+289
-2
lines changed

13 files changed

+289
-2
lines changed

magnum/api/controllers/v1/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from magnum.api.controllers.v1 import certificate
2828
from magnum.api.controllers.v1 import cluster
2929
from magnum.api.controllers.v1 import cluster_template
30+
from magnum.api.controllers.v1 import credential
3031
from magnum.api.controllers.v1 import federation
3132
from magnum.api.controllers.v1 import magnum_services
3233
from magnum.api.controllers.v1 import quota
@@ -98,6 +99,9 @@ class V1(controllers_base.APIBase):
9899
nodegroups = [link.Link]
99100
"""Links to the nodegroups resource"""
100101

102+
credentials = [link.Link]
103+
"""Links to the credentials resource"""
104+
101105
@staticmethod
102106
def convert():
103107
v1 = V1()
@@ -162,6 +166,12 @@ def convert():
162166
'clusters/{cluster_id}',
163167
'nodegroups',
164168
bookmark=True)]
169+
v1.credentials = [link.Link.make_link('self', pecan.request.host_url,
170+
'credentials', ''),
171+
link.Link.make_link('bookmark',
172+
pecan.request.host_url,
173+
'credentials', '',
174+
bookmark=True)]
165175

166176
return v1
167177

@@ -176,6 +186,7 @@ class Controller(controllers_base.Controller):
176186
mservices = magnum_services.MagnumServiceController()
177187
stats = stats.StatsController()
178188
federations = federation.FederationsController()
189+
credentials = credential.CredentialsController()
179190

180191
@expose.expose(V1)
181192
def get(self):
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pecan
14+
from wsme import types as wtypes
15+
16+
from oslo_log import log
17+
18+
from magnum.api.controllers import base
19+
from magnum.api.controllers.v1 import types
20+
from magnum.api import expose
21+
from magnum.api import utils as api_utils
22+
from magnum.common import policy
23+
24+
25+
LOG = log.getLogger(__name__)
26+
27+
28+
class ClusterID(wtypes.Base):
29+
"""API representation of a cluster ID
30+
31+
This class enforces type checking and value constraints, and converts
32+
between the internal object model and the API representation of a cluster
33+
ID.
34+
"""
35+
36+
uuid = types.uuid
37+
"""Unique UUID for this cluster"""
38+
39+
def __init__(self, uuid):
40+
self.uuid = uuid
41+
42+
43+
class CredentialsController(base.Controller):
44+
"""REST controller for cluster actions."""
45+
def __init__(self):
46+
super(CredentialsController, self).__init__()
47+
48+
@base.Controller.api_version("1.12")
49+
@expose.expose(ClusterID, types.uuid_or_name, status_code=202)
50+
def patch(self, cluster_ident):
51+
"""Rotate the credential in use by a cluster.
52+
53+
:param cluster_ident: UUID of a cluster or logical name of the cluster.
54+
"""
55+
56+
context = pecan.request.context
57+
policy.enforce(context, 'credential:rotate',
58+
action='credential:rotate')
59+
60+
cluster = api_utils.get_resource('Cluster', cluster_ident)
61+
# NOTE(northcottmt): Perform rotation synchronously as there aren't any
62+
# slow apply/upgrade operations to do
63+
pecan.request.rpcapi.credential_rotate(cluster)
64+
65+
return ClusterID(cluster.uuid)

magnum/api/controllers/versions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
* 1.9 - Add nodegroup API
3535
* 1.10 - Allow nodegroups with 0 nodes
3636
* 1.11 - Remove bay and baymodel objects
37+
* 1.12 - Add credential API
3738
"""
3839

3940
BASE_VER = '1.1'
40-
CURRENT_MAX_VER = '1.11'
41+
CURRENT_MAX_VER = '1.12'
4142

4243

4344
class Version(object):

magnum/api/rest_api_version_history.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,16 @@ user documentation.
105105
Allow the cluster to be created with node_count = 0 as well as to update
106106
existing nodegroups to have 0 nodes.
107107

108+
108109
1.11
109110
---
110111

111112
Drop bay and baymodels objects from magnum source code
113+
114+
115+
1.12
116+
---
117+
118+
Add credential API
119+
120+
Allow the cluster to have its associated credential rotated.

magnum/cmd/conductor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from magnum.conductor.handlers import ca_conductor
3030
from magnum.conductor.handlers import cluster_conductor
3131
from magnum.conductor.handlers import conductor_listener
32+
from magnum.conductor.handlers import credential_conductor
3233
from magnum.conductor.handlers import federation_conductor
3334
from magnum.conductor.handlers import indirection_api
3435
from magnum.conductor.handlers import nodegroup_conductor
@@ -54,6 +55,7 @@ def main():
5455
indirection_api.Handler(),
5556
cluster_conductor.Handler(),
5657
conductor_listener.Handler(),
58+
credential_conductor.Handler(),
5759
ca_conductor.Handler(),
5860
federation_conductor.Handler(),
5961
nodegroup_conductor.Handler(),

magnum/common/policies/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from magnum.common.policies import certificate
1919
from magnum.common.policies import cluster
2020
from magnum.common.policies import cluster_template
21+
from magnum.common.policies import credential
2122
from magnum.common.policies import federation
2223
from magnum.common.policies import magnum_service
2324
from magnum.common.policies import nodegroup
@@ -31,6 +32,7 @@ def list_rules():
3132
certificate.list_rules(),
3233
cluster.list_rules(),
3334
cluster_template.list_rules(),
35+
credential.list_rules(),
3436
federation.list_rules(),
3537
magnum_service.list_rules(),
3638
quota.list_rules(),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
from oslo_policy import policy
15+
16+
from magnum.common.policies import base
17+
18+
rules = [
19+
policy.DocumentedRuleDefault(
20+
name='credential:rotate',
21+
check_str=base.RULE_PROJECT_MEMBER_DENY_CLUSTER_USER,
22+
scope_types=["project"],
23+
description='Rotate the credential of a cluster.',
24+
operations=[
25+
{
26+
'path': '/v1/credentials/{cluster_uuid}',
27+
'method': 'PATCH'
28+
}
29+
]
30+
)
31+
]
32+
33+
34+
def list_rules():
35+
return rules

magnum/conductor/api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ def nodegroup_update(self, cluster, nodegroup):
175175
def nodegroup_update_async(self, cluster, nodegroup):
176176
self._cast('nodegroup_update', cluster=cluster, nodegroup=nodegroup)
177177

178+
# Credential Operations
179+
180+
def credential_rotate(self, cluster):
181+
return self._call('credential_rotate', cluster=cluster)
182+
183+
def credential_rotate_async(self, cluster):
184+
return self._cast('credential_rotate', cluster=cluster)
185+
178186

179187
@profiler.trace_cls("rpc")
180188
class ListenerAPI(rpc_service.API):
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from oslo_log import log as logging
14+
15+
from magnum.common import exception
16+
from magnum.common import profiler
17+
import magnum.conf
18+
from magnum.drivers.common import driver
19+
from magnum.i18n import _
20+
from magnum.objects import fields
21+
22+
CONF = magnum.conf.CONF
23+
24+
LOG = logging.getLogger(__name__)
25+
26+
ALLOWED_CLUSTER_STATES = {
27+
fields.ClusterStatus.CREATE_COMPLETE,
28+
fields.ClusterStatus.UPDATE_COMPLETE,
29+
fields.ClusterStatus.UPDATE_IN_PROGRESS,
30+
fields.ClusterStatus.UPDATE_FAILED,
31+
fields.ClusterStatus.RESUME_COMPLETE,
32+
fields.ClusterStatus.RESTORE_COMPLETE,
33+
fields.ClusterStatus.ROLLBACK_COMPLETE,
34+
fields.ClusterStatus.SNAPSHOT_COMPLETE,
35+
fields.ClusterStatus.CHECK_COMPLETE,
36+
fields.ClusterStatus.ADOPT_COMPLETE
37+
}
38+
39+
40+
@profiler.trace_cls("rpc")
41+
class Handler(object):
42+
43+
def __init__(self):
44+
super(Handler, self).__init__()
45+
46+
def credential_rotate(self, context, cluster):
47+
if cluster.status not in ALLOWED_CLUSTER_STATES:
48+
operation = _(
49+
f'{__name__} when cluster status is "{cluster.status}"')
50+
raise exception.NotSupported(operation=operation)
51+
52+
cluster_driver = driver.Driver.get_driver_for_cluster(context, cluster)
53+
54+
try:
55+
cluster_driver.rotate_credential(context, cluster)
56+
except NotImplementedError:
57+
raise exception.NotSupported(
58+
message=_("Credential rotation is not supported by driver."))

magnum/drivers/common/driver.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ def delete_nodegroup(self, context, cluster, nodegroup):
257257
raise NotImplementedError("Subclasses must implement "
258258
"'delete_nodegroup'.")
259259

260+
def rotate_credential(self, context, cluster):
261+
raise NotImplementedError(
262+
"Driver does not support credential rotate.")
263+
260264
def get_monitor(self, context, cluster):
261265
"""return the monitor with container data for this driver."""
262266

0 commit comments

Comments
 (0)