|
28 | 28 | from paas_service.auth.backends import InstanceAuthBackend, InstanceAuthFailed |
29 | 29 | from paas_service.auth.decorator import verified_client_required, verified_client_role_require |
30 | 30 | from paas_service.base_vendor import ArgumentInvalidError, InstanceData, OperationFailed, get_provider_cls |
| 31 | +from paas_service.idem_prov import idempotent_provision_instance |
31 | 32 | from paas_service.mixins import LoginRequiredMixin |
32 | | -from paas_service.models import Plan, Service, ServiceInstance, ServiceInstanceConfig |
| 33 | +from paas_service.models import Plan, ProvisionRecord, Service, ServiceInstance, ServiceInstanceConfig |
33 | 34 | from paas_service.utils import parse_redirect_params |
34 | 35 | from rest_framework import status, viewsets |
35 | 36 | from rest_framework.response import Response |
36 | 37 |
|
| 38 | + |
37 | 39 | logger = logging.getLogger(__name__) |
38 | 40 |
|
39 | 41 | m_verified_client_required = method_decorator(verified_client_required) |
@@ -170,6 +172,101 @@ def provision(self, request, service_id, instance_id): |
170 | 172 | service_instance.prerender(request) |
171 | 173 | return Response(serializers.ServiceInstanceSLZ(service_instance).data, status=201) |
172 | 174 |
|
| 175 | + @verified_client_role_require('internal_platform') |
| 176 | + def idem_prov(self, request, service_id): |
| 177 | + """ |
| 178 | + Provision a new instance with idempotency, use it when the provisioning process |
| 179 | + is expected to be slow and caller want to avoid duplicated instance created by retrying |
| 180 | +
|
| 181 | + request example: |
| 182 | + { |
| 183 | + "plan_id": "f8ad12f2-4dd5-4871-8e31-9a1f9f3795a2", |
| 184 | + "params": { |
| 185 | + "engine_app_name": "bkapp-myapp-stag", # required, default provision/idempotent key |
| 186 | + } |
| 187 | + } |
| 188 | +
|
| 189 | + Response: |
| 190 | + [http status code] [description] |
| 191 | + 200 existing instance is reused |
| 192 | + 201 instance provisioned successfully |
| 193 | + 202 instance is still being provisioned, retry later |
| 194 | + 400 bad request, e.g. missing required params or provision key conflict with plan_id |
| 195 | + 500 provider operation failed or unknown error occurred |
| 196 | +
|
| 197 | + Flow: |
| 198 | + Create Req ---> [provision key] ----| |
| 199 | + |---> Record exists && status=SUCCESS |
| 200 | + | && instance.plan=plan |
| 201 | + | ---> Reuse existing instance ---> 200 |
| 202 | + | |
| 203 | + |---> Input params error or conflict: |
| 204 | + | ---> Reject due to plan conflict ---> 400 |
| 205 | + | |
| 206 | + |---> Record exists && status=PROVISIONING |
| 207 | + | ---> Another request is still creating |
| 208 | + | the instance ---> 202 |
| 209 | + | ^ |
| 210 | + | | |
| 211 | + | if provider create takes too long, |
| 212 | + | caller may retry Create Req and will |
| 213 | + | hit this branch until the first request |
| 214 | + | finishes or fails |
| 215 | + | |
| 216 | + |---> No record |
| 217 | + ---> Create PROVISIONING record |
| 218 | + ---> Start provider.create() |
| 219 | + |---> Success |
| 220 | + | ---> Create instance |
| 221 | + | ---> Mark record SUCCESS |
| 222 | + | ---> 201 |
| 223 | + | |
| 224 | + |---> Failed |
| 225 | + ---> Delete record |
| 226 | + ---> 500 |
| 227 | + """ |
| 228 | + plan_id = request.data.get('plan_id') |
| 229 | + params = request.data.get('params', {}) |
| 230 | + engine_app_name = params.get('engine_app_name') |
| 231 | + if not (engine_app_name and plan_id): |
| 232 | + return Response( |
| 233 | + data={'detail': f"{'engine_app_name' if not engine_app_name else 'plan_id'} is required"}, |
| 234 | + status=status.HTTP_400_BAD_REQUEST |
| 235 | + ) |
| 236 | + |
| 237 | + service = get_object_or_404(Service, pk=service_id) |
| 238 | + plan = get_object_or_404(Plan, service=service, pk=plan_id) |
| 239 | + |
| 240 | + with wrap_provider_action_exc('create instance') as ret: |
| 241 | + service_instance, created = idempotent_provision_instance( |
| 242 | + service=service, |
| 243 | + plan=plan, |
| 244 | + provision_key=engine_app_name, |
| 245 | + params=params, |
| 246 | + provider_cls_getter=get_provider_cls, |
| 247 | + ) |
| 248 | + |
| 249 | + if ret.has_error: |
| 250 | + return ret.response |
| 251 | + |
| 252 | + # 分配中... |
| 253 | + if service_instance is None: |
| 254 | + return Response( |
| 255 | + data={'detail': 'instance is being provisioned, please retry later'}, |
| 256 | + status=status.HTTP_202_ACCEPTED |
| 257 | + ) |
| 258 | + |
| 259 | + if service_instance.plan.uuid != plan.uuid: |
| 260 | + return Response( |
| 261 | + data={'detail': 'an instance with the same provision key already exists but with different plan, unable to provision'}, |
| 262 | + status=status.HTTP_400_BAD_REQUEST |
| 263 | + ) |
| 264 | + |
| 265 | + service_instance.prerender(request) |
| 266 | + status_code = status.HTTP_201_CREATED if created else status.HTTP_200_OK |
| 267 | + return Response(serializers.ServiceInstanceSLZ(service_instance).data, status=status_code) |
| 268 | + |
| 269 | + |
173 | 270 | @m_verified_client_required |
174 | 271 | def retrieve(self, request, instance_id): |
175 | 272 | """Retrieve an instance""" |
@@ -231,6 +328,13 @@ def async_destroy(self, request, instance_id): |
231 | 328 | logger.exception("mark instance need to delete failed") |
232 | 329 | return Response(data={'detail': f'unable to mark instance<{instance_id}> to delete: {e}'}) |
233 | 330 |
|
| 331 | + try: |
| 332 | + record = ProvisionRecord.objects.get(service_instance=instance) |
| 333 | + record.delete() |
| 334 | + except ProvisionRecord.DoesNotExist: |
| 335 | + # 兼容线上历史数据无对应 ProvisionRecord 的情况 |
| 336 | + pass |
| 337 | + |
234 | 338 | return Response(status=204) |
235 | 339 |
|
236 | 340 |
|
|
0 commit comments