Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Cloud Endpoints API for Package Repository service.""" | |
| 6 | |
| 7 import endpoints | |
| 8 | |
| 9 from protorpc import message_types | |
| 10 from protorpc import messages | |
| 11 from protorpc import remote | |
| 12 | |
| 13 from components import auth | |
| 14 from components import utils | |
| 15 | |
| 16 from . import acl | |
| 17 from . import impl | |
| 18 | |
| 19 | |
| 20 # This is used by endpoints indirectly. | |
| 21 package = 'cipd' | |
| 22 | |
| 23 | |
| 24 class InstanceMetadata(messages.Message): | |
| 25 """Description of how the package instance was built and registered.""" | |
| 26 date = messages.StringField(1, required=True) | |
| 27 hostname = messages.StringField(2, required=True) | |
| 28 user = messages.StringField(3, required=True) | |
| 29 | |
| 30 # Output only fields. | |
| 31 registered_by = messages.StringField(4, required=False) | |
| 32 registered_ts = messages.IntegerField(5, required=False) | |
| 33 | |
| 34 | |
| 35 def metadata_from_entity(ent): | |
| 36 """PackageInstanceMetadata entity -> InstanceMetadata message.""" | |
| 37 return InstanceMetadata( | |
| 38 date=ent.date, | |
| 39 hostname=ent.hostname, | |
| 40 user=ent.user, | |
| 41 registered_by=ent.registered_by.to_bytes(), | |
| 42 registered_ts=utils.datetime_to_timestamp(ent.registered_ts)) | |
| 43 | |
| 44 | |
| 45 def metadata_to_entity(msg): | |
| 46 """InstanceMetadata message -> PackageInstanceMetadata entity.""" | |
| 47 return impl.PackageInstanceMetadata( | |
| 48 date=msg.date, | |
| 49 hostname=msg.hostname, | |
| 50 user=msg.user) | |
| 51 | |
| 52 | |
| 53 class Signature(messages.Message): | |
| 54 """Single signature. Each package instance can have multiple signatures. | |
| 55 | |
| 56 See also SignatureBlock struct in infra/tools/cipd/common.go. | |
| 57 """ | |
| 58 hash_algo = messages.StringField(1, required=True) | |
| 59 digest = messages.BytesField(2, required=True) | |
| 60 signature_algo = messages.StringField(3, required=True) | |
| 61 signature_key = messages.StringField(4, required=True) | |
| 62 signature = messages.BytesField(5, required=True) | |
| 63 | |
| 64 # Output only fields. | |
| 65 added_by = messages.StringField(6, required=False) | |
| 66 added_ts = messages.IntegerField(7, required=False) | |
| 67 | |
| 68 | |
| 69 def signature_from_entity(ent): | |
| 70 """PackageInstanceSignature entity -> Signature message.""" | |
| 71 return Signature( | |
| 72 hash_algo=ent.hash_algo, | |
| 73 digest=ent.digest, | |
| 74 signature_algo=ent.signature_algo, | |
| 75 signature_key=ent.signature_key, | |
| 76 signature=ent.signature, | |
| 77 added_by=ent.added_by.to_bytes(), | |
| 78 added_ts=utils.datetime_to_timestamp(ent.added_ts)) | |
| 79 | |
| 80 | |
| 81 def signature_to_entity(msg): | |
| 82 """Signature message -> PackageInstanceSignature entity.""" | |
| 83 return impl.PackageInstanceSignature( | |
| 84 hash_algo=msg.hash_algo, | |
| 85 digest=msg.digest, | |
| 86 signature_algo=msg.signature_algo, | |
| 87 signature_key=msg.signature_key, | |
| 88 signature=msg.signature) | |
| 89 | |
| 90 | |
| 91 class RegisterPackageRequest(messages.Message): | |
| 92 """Request to add a new package instance if it is not yet present. | |
| 93 | |
| 94 Instance metadata is recorded only if package instance is not yet present. | |
| 95 Signatures are appended to the list of signatures (even for existing package). | |
| 96 | |
| 97 Callers are expected to execute following protocol: | |
| 98 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.
| |
| 99 2. On UPLOAD_FIRST response, upload package data and finalize the upload by | |
| 100 using upload_session_id and upload_url and calling cas.finishUpload. | |
| 101 3. Once upload is finalized, call registerPackage(msg) again. | |
| 102 """ | |
| 103 package_name = messages.StringField(1, required=True) | |
| 104 instance_id = messages.StringField(2, required=True) | |
| 105 metadata = messages.MessageField(InstanceMetadata, 3, required=True) | |
| 106 signatures = messages.MessageField(Signature, 4, repeated=True) | |
| 107 | |
| 108 | |
| 109 class RegisterPackageResponse(messages.Message): | |
| 110 """Results of registerPackage call. | |
| 111 | |
| 112 upload_session_id and upload_url (if present) can be used with CAS service | |
| 113 (finishUpload call in particular). | |
| 114 """ | |
| 115 class Status(messages.Enum): | |
|
nodir
2014/12/30 22:54:00
Blank line after """
Vadim Sh.
2014/12/31 01:27:35
Done.
| |
| 116 # Package instance successfully registered. | |
| 117 REGISTERED = 1 | |
| 118 # Such package instance already exists. It is not an error. | |
| 119 ALREADY_REGISTERED = 2 | |
| 120 # Package data has to be upload to CAS first. | |
| 121 UPLOAD_FIRST = 3 | |
| 122 # Some unexpected fatal error happened. | |
| 123 ERROR = 4 | |
| 124 | |
| 125 # Status of this operation, defines what other fields to expect. | |
| 126 status = messages.EnumField( | |
| 127 '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
| |
| 128 | |
| 129 # For REGISTERED or ALREADY_REGISTERED a current metadata of package instance. | |
| 130 metadata = messages.MessageField(InstanceMetadata, 2, required=False) | |
| 131 | |
| 132 # For UPLOAD_FIRST status, a unique identifier of the upload operation. | |
| 133 upload_session_id = messages.StringField(3, required=False) | |
| 134 # For UPLOAD_FIRST status, URL to PUT file to via resumable upload protocol. | |
| 135 upload_url = messages.StringField(4, required=False) | |
| 136 | |
| 137 # For ERROR status, a error message. | |
| 138 error_message = messages.StringField(5, required=False) | |
| 139 | |
| 140 | |
| 141 @auth.endpoints_api( | |
| 142 name='repo', | |
| 143 version='v1', | |
| 144 title='Package Repository API') | |
| 145 class PackageRepositoryApi(remote.Service): | |
| 146 """Package Repository API.""" | |
| 147 | |
| 148 @auth.endpoints_method( | |
| 149 RegisterPackageRequest, | |
| 150 RegisterPackageResponse, | |
| 151 http_method='POST', | |
| 152 name='registerPackage') | |
| 153 @auth.require(lambda: not auth.get_current_identity().is_anonymous) | |
| 154 def register_package(self, request): | |
| 155 """Registers a new package instance in the repository.""" | |
| 156 if not impl.is_valid_package_name(request.package_name): | |
| 157 raise endpoints.BadRequestException('Invalid package name') | |
| 158 if not impl.is_valid_instance_id(request.instance_id): | |
| 159 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
| |
| 160 | |
|
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.
| |
| 161 caller = auth.get_current_identity() | |
| 162 if not acl.can_register_package(request.package_name, caller): | |
| 163 raise auth.AuthorizationError() | |
| 164 | |
| 165 service = impl.get_repo_service() | |
| 166 if service is None: | |
| 167 raise endpoints.InternalServerErrorException('Service is not configured') | |
| 168 | |
| 169 # Metadata proto -> entity. | |
| 170 metadata = metadata_to_entity(request.metadata) | |
| 171 metadata.registered_by = caller | |
| 172 metadata.registered_ts = utils.utcnow() | |
| 173 | |
| 174 # Signature list proto -> entity. | |
| 175 signatures = [] | |
| 176 for sig in request.signatures: | |
| 177 ent = signature_to_entity(sig) | |
| 178 ent.added_by = caller | |
| 179 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.
| |
| 180 signatures.append(ent) | |
| 181 | |
| 182 # Already registered? Just attach any new signatures. | |
| 183 pkg = service.get_instance(request.package_name, request.instance_id) | |
| 184 if pkg is not None: | |
| 185 service.add_signatures( | |
| 186 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
| |
| 187 return RegisterPackageResponse( | |
| 188 status=RegisterPackageResponse.Status.ALREADY_REGISTERED, | |
| 189 metadata=metadata_from_entity(pkg.metadata)) | |
| 190 | |
| 191 # Need to upload to CAS first? Open an upload session. Caller must use | |
| 192 # CASServiceApi to finish the upload and then call registerPackage again. | |
| 193 if not service.is_data_uploaded(request.package_name, request.instance_id): | |
| 194 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
| |
| 195 request.package_name, request.instance_id, caller) | |
| 196 return RegisterPackageResponse( | |
| 197 status=RegisterPackageResponse.Status.UPLOAD_FIRST, | |
| 198 upload_session_id=upload_session_id, | |
| 199 upload_url=upload_url) | |
| 200 | |
| 201 # Package data is in the store. Make an entity. | |
| 202 pkg, registered = service.register_instance( | |
| 203 request.package_name, request.instance_id, metadata, signatures) | |
| 204 if registered: | |
| 205 status = RegisterPackageResponse.Status.REGISTERED | |
| 206 else: # pragma: no cover | |
| 207 status = RegisterPackageResponse.Status.ALREADY_REGISTERED | |
| 208 return RegisterPackageResponse( | |
| 209 status=status, | |
| 210 metadata=metadata_from_entity(pkg.metadata)) | |
| OLD | NEW |