Skip to content

Commit e21e153

Browse files
authored
Merge pull request #13 from jianxiaoguo/master
feat(controller):add drycc resource cmd
2 parents 3aa5214 + 0b503a1 commit e21e153

12 files changed

Lines changed: 812 additions & 2 deletions

File tree

charts/controller/templates/controller-clusterrole.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,11 @@ rules:
6565
- apiGroups: [""]
6666
resources: ["persistentvolumeclaims"]
6767
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
68+
- apiGroups: ["servicecatalog.k8s.io"]
69+
resources: ["serviceinstances"]
70+
verbs: ["get", "list", "watch", "create", "delete", "update"]
71+
- apiGroups: ["servicecatalog.k8s.io"]
72+
resources: ["serviceinstances"]
73+
verbs: ["get", "list", "watch", "create", "delete", "update"]
6874
{{- end -}}
6975
{{- end -}}

rootfs/api/migrations/0001_initial.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,25 @@ class Migration(migrations.Migration):
251251
'unique_together': {('app', 'name')},
252252
},
253253
),
254+
migrations.CreateModel(
255+
name='Resource',
256+
fields=[
257+
('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')),
258+
('created', models.DateTimeField(auto_now_add=True)),
259+
('updated', models.DateTimeField(auto_now=True)),
260+
('name', models.CharField(max_length=63, validators=[api.models.validate_label])),
261+
('plan', models.CharField(max_length=128)),
262+
('data', jsonfield.fields.JSONField(blank=True, default={})),
263+
('status', models.CharField(max_length=32, null=True)),
264+
('binding', models.CharField(max_length=32, null=True)),
265+
('options', jsonfield.fields.JSONField(blank=True, default={})),
266+
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
267+
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
268+
],
269+
options={
270+
'ordering': ['-created'],
271+
'get_latest_by': 'created',
272+
'unique_together': {('app', 'name')},
273+
},
274+
),
254275
]

rootfs/api/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class Meta:
151151
from .release import Release # noqa
152152
from .tls import TLS # noqa
153153
from .volume import Volume # noqa
154+
from .resource import Resource # noqa
154155

155156
# define update/delete callbacks for synchronizing
156157
# models with the configuration management backend
@@ -248,6 +249,7 @@ def _hook_release_created(**kwargs):
248249
post_delete.connect(_log_instance_removed, sender=Domain, dispatch_uid='api.models.log')
249250
post_delete.connect(_log_instance_removed, sender=TLS, dispatch_uid='api.models.log')
250251
post_delete.connect(_log_instance_removed, sender=Volume, dispatch_uid='api.models.log')
252+
post_delete.connect(_log_instance_removed, sender=Resource, dispatch_uid='api.models.log')
251253

252254

253255
# automatically generate a new token on creation

rootfs/api/models/resource.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import logging
2+
import uuid
3+
import time
4+
from django.core import signals
5+
from django.conf import settings
6+
from django.db import models, transaction
7+
from jsonfield import JSONField
8+
from api.exceptions import DryccException, AlreadyExists, ServiceUnavailable
9+
from api.models import UuidAuditedModel, validate_label
10+
from scheduler import KubeException
11+
from tasks import task, apply_async
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class Resource(UuidAuditedModel):
17+
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
18+
on_delete=models.PROTECT)
19+
app = models.ForeignKey('App', on_delete=models.CASCADE)
20+
name = models.CharField(max_length=63, validators=[validate_label])
21+
plan = models.CharField(max_length=128)
22+
data = JSONField(default={}, blank=True)
23+
status = models.CharField(max_length=32, null=True)
24+
binding = models.CharField(max_length=32, null=True)
25+
options = JSONField(default={}, blank=True)
26+
27+
class Meta:
28+
get_latest_by = 'created'
29+
unique_together = (('app', 'name'),)
30+
ordering = ['-created']
31+
32+
def __str__(self):
33+
return self.name
34+
35+
@transaction.atomic
36+
def save(self, *args, **kwargs):
37+
# Attach ServiceInstance, updates k8s
38+
if self.created == self.updated:
39+
self.attach(*args, **kwargs)
40+
# Save to DB
41+
return super(Resource, self).save(*args, **kwargs)
42+
43+
def attach(self, *args, **kwargs):
44+
try:
45+
self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
46+
err = "Resource {} already exists in this namespace".format(self.name) # noqa
47+
self.log(err, logging.INFO)
48+
raise AlreadyExists(err)
49+
except KubeException as e:
50+
logger.info(e)
51+
try:
52+
instance = self.plan.split(":")
53+
kwargs = {
54+
"instance_class": instance[0],
55+
"instance_plan": ":".join(instance[1:]),
56+
"parameters": self.options,
57+
}
58+
self._scheduler.servicecatalog.create_instance(
59+
self.app.id, self.name, **kwargs
60+
)
61+
# create/patch/put retrieve_task
62+
data = {
63+
"task_id": uuid.uuid4().hex,
64+
"resource_id": str(self.uuid),
65+
}
66+
apply_async(retrieve_task, delay=30000, args=(data, ))
67+
except KubeException as e:
68+
msg = 'There was a problem creating the resource ' \
69+
'{} for {}'.format(self.name, self.app_id)
70+
raise ServiceUnavailable(msg) from e
71+
72+
@transaction.atomic
73+
def delete(self, *args, **kwargs):
74+
if self.binding and self.binding == "Ready":
75+
raise DryccException("the plan is still binding")
76+
# Deatch ServiceInstance, updates k8s
77+
self.detach(*args, **kwargs)
78+
# Delete from DB
79+
return super(Resource, self).delete(*args, **kwargs)
80+
81+
def detach(self, *args, **kwargs):
82+
try:
83+
# We raise an exception when a resource doesn't exist
84+
self._scheduler.servicecatalog.get_instance(self.app.id, self.name)
85+
self._scheduler.servicecatalog.delete_instance(self.app.id, self.name)
86+
except KubeException as e:
87+
raise ServiceUnavailable("Could not delete volume {} for application {}".format(name, self.app_id)) from e # noqa
88+
89+
def log(self, message, level=logging.INFO):
90+
"""Logs a message in the context of this service.
91+
92+
This prefixes log messages with an application "tag" that the customized
93+
drycc-logspout will be on the lookout for. When it's seen, the message-- usually
94+
an application event of some sort like releasing or scaling, will be considered
95+
as "belonging" to the application instead of the controller and will be handled
96+
accordingly.
97+
"""
98+
logger.log(level, "[{}]: {}".format(self.id, message))
99+
100+
def bind(self, *args, **kwargs):
101+
if self.status != "Ready":
102+
raise DryccException("the resource is not ready")
103+
if self.binding == "Ready":
104+
raise DryccException("the resource is binding")
105+
try:
106+
self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
107+
err = "Resource {} is binding".format(self.name)
108+
self.log(err, logging.INFO)
109+
raise AlreadyExists(err)
110+
except KubeException as e:
111+
logger.info(e)
112+
try:
113+
self._scheduler.servicecatalog.create_binding(
114+
self.app.id, self.name, **kwargs)
115+
# create/patch/put retrieve_task
116+
data = {
117+
"task_id": uuid.uuid4().hex,
118+
"resource_id": str(self.uuid),
119+
}
120+
apply_async(retrieve_task, delay=30000, args=(data, ))
121+
except KubeException as e:
122+
msg = 'There was a problem binding the resource ' \
123+
'{} for {}'.format(self.name, self.app_id)
124+
raise ServiceUnavailable(msg) from e
125+
126+
def unbind(self, *args, **kwargs):
127+
if self.binding != "Ready":
128+
raise DryccException("the resource is not binding")
129+
try:
130+
# We raise an exception when a resource doesn't exist
131+
self._scheduler.servicecatalog.get_binding(self.app.id, self.name)
132+
self._scheduler.servicecatalog.delete_binding(self.app.id, self.name)
133+
except KubeException as e:
134+
raise ServiceUnavailable("Could not unbind resource {} for application {}".format(self.name, self.app_id)) from e # noqa
135+
136+
def attach_update(self, *args, **kwargs):
137+
try:
138+
data = self._scheduler.servicecatalog.get_instance(
139+
self.app.id, self.name).json()
140+
except KubeException as e:
141+
self.log("certificate {} does not exist".format(self.app.id),
142+
level=logging.INFO)
143+
data = None
144+
logger.info(e)
145+
try:
146+
version = data["metadata"]["resourceVersion"]
147+
instance = self.plan.split(":")
148+
kwargs = {
149+
"instance_class": instance[0],
150+
"instance_plan": ":".join(instance[1:]),
151+
"parameters": self.options,
152+
}
153+
self._scheduler.servicecatalog.put_instance(
154+
self.app.id, self.name, version, **kwargs
155+
)
156+
# create/patch/put retrieve_task
157+
data = {
158+
"task_id": uuid.uuid4().hex,
159+
"resource_id": str(self.uuid),
160+
}
161+
apply_async(retrieve_task, delay=30000, args=(data, ))
162+
except KubeException as e:
163+
msg = 'There was a problem update the resource ' \
164+
'{} for {}'.format(self.name, self.app_id)
165+
raise ServiceUnavailable(msg) from e
166+
167+
def retrieve(self, *args, **kwargs):
168+
update_flag = False
169+
if self.status != "Ready":
170+
try:
171+
resp_i = self._scheduler.servicecatalog.get_instance(
172+
self.app.id, self.name).json()
173+
self.status = resp_i.get('status', {}).\
174+
get('lastConditionState', '').lower()
175+
update_flag = True
176+
except KubeException as e:
177+
logger.info("retrieve instance info error: {}".format(e))
178+
if self.binding != "Ready":
179+
try:
180+
# We raise an exception when a resource doesn't exist
181+
resp_b = self._scheduler.servicecatalog.get_binding(
182+
self.app.id, self.name).json()
183+
self.binding = resp_b.get('status', {}).\
184+
get('lastConditionState', '').lower()
185+
self.options = resp_b.get('spec', {}).get('parameters', {})
186+
update_flag = True
187+
secret_name = resp_b.get('spec', {}).get('secretName')
188+
if secret_name:
189+
resp_s = self._scheduler.secret.get(
190+
self.app.id, secret_name).json()
191+
self.data = resp_s.get('data', {})
192+
update_flag = True
193+
except KubeException as e:
194+
logger.info("retrieve binding info error: {}".format(e))
195+
if update_flag is True:
196+
self.save()
197+
if self.status and self.binding:
198+
return True
199+
else:
200+
return False
201+
202+
def detach_resource(self, *args, **kwargs):
203+
if self.binding != "Ready":
204+
try:
205+
resp_b = self._scheduler.servicecatalog.get_binding(
206+
self.app.id, self.name).json()
207+
secret_name = resp_b.get('spec', {}).get('secretName')
208+
if secret_name:
209+
self._scheduler.secret.delete(self.app.id, secret_name)
210+
self._scheduler.servicecatalog.delete_binding(
211+
self.app.id, self.name)
212+
except KubeException as e:
213+
logger.info("delete binding info error: {}".format(e))
214+
self.binding = None
215+
216+
if (self.status != "Ready") or (self.binding is None):
217+
try:
218+
self._scheduler.servicecatalog.delete_instance(
219+
self.app.id, self.name)
220+
except KubeException as e:
221+
logger.info("retrieve instance info error: {}".format(e))
222+
223+
224+
@task
225+
def retrieve_task(data):
226+
try:
227+
signals.request_started.send(sender=data['task_id'])
228+
try:
229+
resource = Resource.objects.get(uuid=data['resource_id'])
230+
except Resource.DoesNotExist:
231+
logger.info("retrieve task not found resource: {}".format(data['resource_id'])) # noqa
232+
return True
233+
_ = resource.retrieve()
234+
if _:
235+
return True
236+
else:
237+
t = time.time() - resource.created.timestamps()
238+
if t < 3600:
239+
apply_async(retrieve_task, delay=30000, args=(data, ))
240+
elif t < 3600 * 12:
241+
apply_async(retrieve_task, delay=1800000, args=(data, ))
242+
else:
243+
resource.detach_resource()
244+
finally:
245+
signals.request_finished.send(sender=data['task_id'])

rootfs/api/models/volume.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,14 @@ def detach(self, *args, **kwargs):
6868
except KubeException as e:
6969
raise ServiceUnavailable("Could not delete volume {} for application \
7070
{}".format(name, self.app_id)) from e # noqa
71+
72+
def log(self, message, level=logging.INFO):
73+
"""Logs a message in the context of this service.
74+
75+
This prefixes log messages with an application "tag" that the customized
76+
drycc-logspout will be on the lookout for. When it's seen, the message-- usually
77+
an application event of some sort like releasing or scaling, will be considered
78+
as "belonging" to the application instead of the controller and will be handled
79+
accordingly.
80+
"""
81+
logger.log(level, "[{}]: {}".format(self.id, message))

rootfs/api/serializers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
# proc type name is lowercase alphanumeric
2121
# https://docs-v2.readthedocs.io/en/latest/using-workflow/process-types-and-the-procfile/#declaring-process-types
22+
from api.exceptions import DryccException
23+
2224
PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z0-9]+(\-[a-z0-9]+)*)$')
2325
PROCTYPE_MISMATCH_MSG = "Process types can only contain lowercase alphanumeric characters"
2426
MEMLIMIT_MATCH = re.compile(
@@ -649,3 +651,27 @@ def validate_path(self, data):
649651
raise serializers.ValidationError(
650652
"Volume path format: /path")
651653
return data
654+
655+
656+
class ResourceSerializer(serializers.ModelSerializer):
657+
"""Serialize a :class:`~api.models.Resource` model."""
658+
app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all())
659+
owner = serializers.ReadOnlyField(source='owner.username')
660+
name = serializers.CharField(max_length=63, required=True)
661+
plan = serializers.CharField(max_length=128, required=True)
662+
options = JSONFieldSerializer(required=False, binary=True)
663+
664+
class Meta:
665+
"""Metadata options for a :class:`ResourceSerializer`."""
666+
model = models.Resource
667+
fields = '__all__'
668+
669+
def update(self, instance, validated_data):
670+
if instance.plan.split(':')[0] != validated_data.get('plan', '').split(':')[0]: # noqa
671+
raise DryccException("the resource cann't changed")
672+
instance.plan = validated_data.get('plan')
673+
if validated_data.get('options'):
674+
instance.options = validated_data.get('options')
675+
instance.attach_update()
676+
instance.save()
677+
return instance

0 commit comments

Comments
 (0)