Index: appengine/chrome_infra_packages/cipd/api.py |
diff --git a/appengine/chrome_infra_packages/cipd/api.py b/appengine/chrome_infra_packages/cipd/api.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..51d46deb445e8f084b309c1d01b759e764935e1f |
--- /dev/null |
+++ b/appengine/chrome_infra_packages/cipd/api.py |
@@ -0,0 +1,210 @@ |
+# Copyright 2014 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Cloud Endpoints API for Package Repository service.""" |
+ |
+import endpoints |
+ |
+from protorpc import message_types |
+from protorpc import messages |
+from protorpc import remote |
+ |
+from components import auth |
+from components import utils |
+ |
+from . import acl |
+from . import impl |
+ |
+ |
+# This is used by endpoints indirectly. |
+package = 'cipd' |
+ |
+ |
+class InstanceMetadata(messages.Message): |
+ """Description of how the package instance was built and registered.""" |
+ date = messages.StringField(1, required=True) |
+ hostname = messages.StringField(2, required=True) |
+ user = messages.StringField(3, required=True) |
+ |
+ # Output only fields. |
+ registered_by = messages.StringField(4, required=False) |
+ registered_ts = messages.IntegerField(5, required=False) |
+ |
+ |
+def metadata_from_entity(ent): |
+ """PackageInstanceMetadata entity -> InstanceMetadata message.""" |
+ return InstanceMetadata( |
+ date=ent.date, |
+ hostname=ent.hostname, |
+ user=ent.user, |
+ registered_by=ent.registered_by.to_bytes(), |
+ registered_ts=utils.datetime_to_timestamp(ent.registered_ts)) |
+ |
+ |
+def metadata_to_entity(msg): |
+ """InstanceMetadata message -> PackageInstanceMetadata entity.""" |
+ return impl.PackageInstanceMetadata( |
+ date=msg.date, |
+ hostname=msg.hostname, |
+ user=msg.user) |
+ |
+ |
+class Signature(messages.Message): |
+ """Single signature. Each package instance can have multiple signatures. |
+ |
+ See also SignatureBlock struct in infra/tools/cipd/common.go. |
+ """ |
+ hash_algo = messages.StringField(1, required=True) |
+ digest = messages.BytesField(2, required=True) |
+ signature_algo = messages.StringField(3, required=True) |
+ signature_key = messages.StringField(4, required=True) |
+ signature = messages.BytesField(5, required=True) |
+ |
+ # Output only fields. |
+ added_by = messages.StringField(6, required=False) |
+ added_ts = messages.IntegerField(7, required=False) |
+ |
+ |
+def signature_from_entity(ent): |
+ """PackageInstanceSignature entity -> Signature message.""" |
+ return Signature( |
+ hash_algo=ent.hash_algo, |
+ digest=ent.digest, |
+ signature_algo=ent.signature_algo, |
+ signature_key=ent.signature_key, |
+ signature=ent.signature, |
+ added_by=ent.added_by.to_bytes(), |
+ added_ts=utils.datetime_to_timestamp(ent.added_ts)) |
+ |
+ |
+def signature_to_entity(msg): |
+ """Signature message -> PackageInstanceSignature entity.""" |
+ return impl.PackageInstanceSignature( |
+ hash_algo=msg.hash_algo, |
+ digest=msg.digest, |
+ signature_algo=msg.signature_algo, |
+ signature_key=msg.signature_key, |
+ signature=msg.signature) |
+ |
+ |
+class RegisterPackageRequest(messages.Message): |
+ """Request to add a new package instance if it is not yet present. |
+ |
+ Instance metadata is recorded only if package instance is not yet present. |
+ Signatures are appended to the list of signatures (even for existing package). |
+ |
+ Callers are expected to execute following protocol: |
+ 1. Attempt to register a package instance by callling registerPackage(msg). |
nodir
2014/12/30 22:54:00
typo: calllling
Vadim Sh.
2014/12/31 01:27:35
Done.
|
+ 2. On UPLOAD_FIRST response, upload package data and finalize the upload by |
+ using upload_session_id and upload_url and calling cas.finishUpload. |
+ 3. Once upload is finalized, call registerPackage(msg) again. |
+ """ |
+ package_name = messages.StringField(1, required=True) |
+ instance_id = messages.StringField(2, required=True) |
+ metadata = messages.MessageField(InstanceMetadata, 3, required=True) |
+ signatures = messages.MessageField(Signature, 4, repeated=True) |
+ |
+ |
+class RegisterPackageResponse(messages.Message): |
+ """Results of registerPackage call. |
+ |
+ upload_session_id and upload_url (if present) can be used with CAS service |
+ (finishUpload call in particular). |
+ """ |
+ class Status(messages.Enum): |
nodir
2014/12/30 22:54:00
Blank line after """
Vadim Sh.
2014/12/31 01:27:35
Done.
|
+ # Package instance successfully registered. |
+ REGISTERED = 1 |
+ # Such package instance already exists. It is not an error. |
+ ALREADY_REGISTERED = 2 |
+ # Package data has to be upload to CAS first. |
+ UPLOAD_FIRST = 3 |
+ # Some unexpected fatal error happened. |
+ ERROR = 4 |
+ |
+ # Status of this operation, defines what other fields to expect. |
+ status = messages.EnumField( |
+ 'RegisterPackageResponse.Status', 1, required=True) |
nodir
2014/12/30 22:54:00
Why not EnumField(Status, ...) ?
Vadim Sh.
2014/12/31 01:27:35
Done. For some reason I though it does like local
|
+ |
+ # For REGISTERED or ALREADY_REGISTERED a current metadata of package instance. |
+ metadata = messages.MessageField(InstanceMetadata, 2, required=False) |
+ |
+ # For UPLOAD_FIRST status, a unique identifier of the upload operation. |
+ upload_session_id = messages.StringField(3, required=False) |
+ # For UPLOAD_FIRST status, URL to PUT file to via resumable upload protocol. |
+ upload_url = messages.StringField(4, required=False) |
+ |
+ # For ERROR status, a error message. |
+ error_message = messages.StringField(5, required=False) |
+ |
+ |
+@auth.endpoints_api( |
+ name='repo', |
+ version='v1', |
+ title='Package Repository API') |
+class PackageRepositoryApi(remote.Service): |
+ """Package Repository API.""" |
+ |
+ @auth.endpoints_method( |
+ RegisterPackageRequest, |
+ RegisterPackageResponse, |
+ http_method='POST', |
+ name='registerPackage') |
+ @auth.require(lambda: not auth.get_current_identity().is_anonymous) |
+ def register_package(self, request): |
+ """Registers a new package instance in the repository.""" |
+ if not impl.is_valid_package_name(request.package_name): |
+ raise endpoints.BadRequestException('Invalid package name') |
+ if not impl.is_valid_instance_id(request.instance_id): |
+ raise endpoints.BadRequestException('Invalid instance ID') |
nodir
2014/12/30 22:54:00
Consider returning different error reasons. A prog
Vadim Sh.
2014/12/31 01:27:35
Client will do client side validation. Checks here
|
+ |
nodir
2014/12/30 22:54:00
metadata is not validated? To me, either it should
Vadim Sh.
2014/12/31 01:27:35
I was planing to add very few required FYI only fi
nodir
2015/01/02 19:13:03
Acknowledged.
|
+ caller = auth.get_current_identity() |
+ if not acl.can_register_package(request.package_name, caller): |
+ raise auth.AuthorizationError() |
+ |
+ service = impl.get_repo_service() |
+ if service is None: |
+ raise endpoints.InternalServerErrorException('Service is not configured') |
+ |
+ # Metadata proto -> entity. |
+ metadata = metadata_to_entity(request.metadata) |
+ metadata.registered_by = caller |
+ metadata.registered_ts = utils.utcnow() |
+ |
+ # Signature list proto -> entity. |
+ signatures = [] |
+ for sig in request.signatures: |
+ ent = signature_to_entity(sig) |
+ ent.added_by = caller |
+ ent.added_ts = utils.utcnow() |
nodir
2014/12/30 22:54:00
call utcnow once per request
Vadim Sh.
2014/12/31 01:27:35
Done.
|
+ signatures.append(ent) |
+ |
+ # Already registered? Just attach any new signatures. |
+ pkg = service.get_instance(request.package_name, request.instance_id) |
+ if pkg is not None: |
+ service.add_signatures( |
+ request.package_name, request.instance_id, signatures) |
nodir
2014/12/30 22:54:00
I think you should not add signatures
Vadim Sh.
2014/12/31 01:27:35
Then there's no way to resign a package. I mean it
|
+ return RegisterPackageResponse( |
+ status=RegisterPackageResponse.Status.ALREADY_REGISTERED, |
+ metadata=metadata_from_entity(pkg.metadata)) |
+ |
+ # Need to upload to CAS first? Open an upload session. Caller must use |
+ # CASServiceApi to finish the upload and then call registerPackage again. |
+ if not service.is_data_uploaded(request.package_name, request.instance_id): |
+ upload_url, upload_session_id = service.create_upload_session( |
Vadim Sh.
2014/12/30 02:22:26
It makes 'cas' service and 'repo' service a bit ta
|
+ request.package_name, request.instance_id, caller) |
+ return RegisterPackageResponse( |
+ status=RegisterPackageResponse.Status.UPLOAD_FIRST, |
+ upload_session_id=upload_session_id, |
+ upload_url=upload_url) |
+ |
+ # Package data is in the store. Make an entity. |
+ pkg, registered = service.register_instance( |
+ request.package_name, request.instance_id, metadata, signatures) |
+ if registered: |
+ status = RegisterPackageResponse.Status.REGISTERED |
+ else: # pragma: no cover |
+ status = RegisterPackageResponse.Status.ALREADY_REGISTERED |
+ return RegisterPackageResponse( |
+ status=status, |
+ metadata=metadata_from_entity(pkg.metadata)) |