def tags(self, session, namespace, repo_name, page_size=2, credentials=None, options=None, expected_failure=None): options = options or ProtocolOptions() scopes = options.scopes or [ 'repository:%s:pull' % self.repo_name(namespace, repo_name) ] # Ping! self.ping(session) # Perform auth and retrieve a token. headers = {} if credentials is not None: token, _ = self.auth(session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure) if token is None: return None headers = { 'Authorization': 'Bearer ' + token, } results = [] url = '/v2/%s/tags/list' % (self.repo_name(namespace, repo_name)) params = {} if page_size is not None: params['n'] = page_size while True: response = self.conduct( session, 'GET', url, headers=headers, params=params, expected_status=(200, expected_failure, V2ProtocolSteps.LIST_TAGS)) data = response.json() assert len(data['tags']) <= page_size results.extend(data['tags']) if not response.headers.get('Link'): return results link_url = response.headers['Link'] v2_index = link_url.find('/v2/') url = link_url[v2_index:] return results
def delete( self, session, namespace, repo_name, tag_names, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() scopes = options.scopes or [ "repository:%s:*" % self.repo_name(namespace, repo_name) ] tag_names = [tag_names] if isinstance(tag_names, str) else tag_names # Ping! self.ping(session) # Perform auth and retrieve a token. token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None: return None headers = { "Authorization": "Bearer " + token, } for tag_name in tag_names: self.conduct( session, "DELETE", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), tag_name), headers=headers, expected_status=202, )
def pull( self, session, namespace, repo_name, tag_names, images, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() auth = self._auth_for_credentials(credentials) tag_names = [tag_names] if isinstance(tag_names, str) else tag_names prefix = "/v1/repositories/%s/" % self.repo_name(namespace, repo_name) # Ping! self.ping(session) # GET /v1/repositories/{namespace}/{repository}/images headers = {"X-Docker-Token": "true"} result = self.conduct( session, "GET", prefix + "images", auth=auth, headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_IMAGES), ) if result.status_code != 200: return headers = {} if credentials is not None: headers["Authorization"] = "token " + result.headers[ "www-authenticate"] else: assert not "www-authenticate" in result.headers # GET /v1/repositories/{namespace}/{repository}/tags image_ids = self.conduct(session, "GET", prefix + "tags", headers=headers).json() for tag_name in tag_names: # GET /v1/repositories/{namespace}/{repository}/tags/<tag_name> image_id_data = self.conduct( session, "GET", prefix + "tags/" + tag_name, headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_TAG), ) if tag_name not in image_ids: assert expected_failure == Failures.UNKNOWN_TAG return None tag_image_id = image_ids[tag_name] assert image_id_data.json() == tag_image_id # Retrieve the ancestry of the tagged image. image_prefix = "/v1/images/%s/" % tag_image_id ancestors = self.conduct(session, "GET", image_prefix + "ancestry", headers=headers).json() assert len(ancestors) == len(images) for index, image_id in enumerate(reversed(ancestors)): # /v1/images/{imageID}/{ancestry, json, layer} image_prefix = "/v1/images/%s/" % image_id self.conduct(session, "GET", image_prefix + "ancestry", headers=headers) result = self.conduct(session, "GET", image_prefix + "json", headers=headers) assert result.json()["id"] == image_id # Ensure we can HEAD the image layer. self.conduct(session, "HEAD", image_prefix + "layer", headers=headers) # And retrieve the layer data. result = self.conduct( session, "GET", image_prefix + "layer", headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_LAYER), options=options, ) if result.status_code == 200: assert result.content == images[index].bytes return PullResult(manifests=None, image_ids=image_ids)
def pull(self, session, namespace, repo_name, tag_names, images, credentials=None, expected_failure=None, options=None): options = options or ProtocolOptions() auth = self._auth_for_credentials(credentials) tag_names = [tag_names] if isinstance(tag_names, str) else tag_names prefix = '/v1/repositories/%s/' % self.repo_name(namespace, repo_name) # Ping! self.ping(session) # GET /v1/repositories/{namespace}/{repository}/images headers = {'X-Docker-Token': 'true'} result = self.conduct(session, 'GET', prefix + 'images', auth=auth, headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_IMAGES)) if result.status_code != 200: return headers = {} if credentials is not None: headers['Authorization'] = 'token ' + result.headers[ 'www-authenticate'] else: assert not 'www-authenticate' in result.headers # GET /v1/repositories/{namespace}/{repository}/tags image_ids = self.conduct(session, 'GET', prefix + 'tags', headers=headers).json() for tag_name in tag_names: # GET /v1/repositories/{namespace}/{repository}/tags/<tag_name> image_id_data = self.conduct( session, 'GET', prefix + 'tags/' + tag_name, headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_TAG)) if tag_name not in image_ids: assert expected_failure == Failures.UNKNOWN_TAG return None tag_image_id = image_ids[tag_name] assert image_id_data.json() == tag_image_id # Retrieve the ancestry of the tagged image. image_prefix = '/v1/images/%s/' % tag_image_id ancestors = self.conduct(session, 'GET', image_prefix + 'ancestry', headers=headers).json() assert len(ancestors) == len(images) for index, image_id in enumerate(reversed(ancestors)): # /v1/images/{imageID}/{ancestry, json, layer} image_prefix = '/v1/images/%s/' % image_id self.conduct(session, 'GET', image_prefix + 'ancestry', headers=headers) result = self.conduct(session, 'GET', image_prefix + 'json', headers=headers) assert result.json()['id'] == image_id # Ensure we can HEAD the image layer. self.conduct(session, 'HEAD', image_prefix + 'layer', headers=headers) # And retrieve the layer data. result = self.conduct( session, 'GET', image_prefix + 'layer', headers=headers, expected_status=(200, expected_failure, V1ProtocolSteps.GET_LAYER), options=options) if result.status_code == 200: assert result.content == images[index].bytes return PullResult(manifests=None, image_ids=image_ids)
def catalog( self, session, page_size=2, credentials=None, options=None, expected_failure=None, namespace=None, repo_name=None, bearer_token=None, ): options = options or ProtocolOptions() scopes = options.scopes or [] # Ping! self.ping(session) # Perform auth and retrieve a token. headers = {} if credentials is not None: token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None: return None headers = { "Authorization": "Bearer " + token, } if bearer_token is not None: headers = { "Authorization": "Bearer " + bearer_token, } results = [] url = "/v2/_catalog" params = {} if page_size is not None: params["n"] = page_size while True: response = self.conduct( session, "GET", url, headers=headers, params=params, expected_status=(200, expected_failure, V2ProtocolSteps.CATALOG), ) data = response.json() assert len(data["repositories"]) <= page_size results.extend(data["repositories"]) if not response.headers.get("Link"): return results link_url = response.headers["Link"] v2_index = link_url.find("/v2/") url = link_url[v2_index:] return results
def pull( self, session, namespace, repo_name, tag_names, images, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() scopes = options.scopes or ["repository:%s:pull" % self.repo_name(namespace, repo_name)] tag_names = [tag_names] if isinstance(tag_names, str) else tag_names # Ping! self.ping(session) # Perform auth and retrieve a token. token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None and not options.attempt_pull_without_token: return None headers = {} if token: headers = { "Authorization": "Bearer " + token, } if self.schema == "oci": headers["Accept"] = ",".join( options.accept_mimetypes if options.accept_mimetypes is not None else OCI_CONTENT_TYPES ) elif self.schema == "schema2": headers["Accept"] = ",".join( options.accept_mimetypes if options.accept_mimetypes is not None else DOCKER_SCHEMA2_CONTENT_TYPES ) manifests = {} image_ids = {} for tag_name in tag_names: # Retrieve the manifest for the tag or digest. response = self.conduct( session, "GET", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), tag_name), expected_status=(200, expected_failure, V2ProtocolSteps.GET_MANIFEST), headers=headers, ) if response.status_code == 401: assert "WWW-Authenticate" in response.headers response.encoding = "utf-8" if expected_failure is not None: return None # Ensure the manifest returned by us is valid. ct = response.headers["Content-Type"] if self.schema == "schema1": assert ct in DOCKER_SCHEMA1_CONTENT_TYPES if options.require_matching_manifest_type: if self.schema == "schema1": assert ct in DOCKER_SCHEMA1_CONTENT_TYPES if self.schema == "schema2": assert ct in DOCKER_SCHEMA2_CONTENT_TYPES if self.schema == "oci": assert ct in OCI_CONTENT_TYPES manifest = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text), ct) manifests[tag_name] = manifest if manifest.schema_version == 1: image_ids[tag_name] = manifest.leaf_layer_v1_image_id # Verify the blobs. layer_index = 0 empty_count = 0 blob_digests = list(manifest.blob_digests) for image in images: if manifest.schema_version == 2 and image.is_empty: empty_count += 1 continue # If the layer is remote, then we expect the blob to *not* exist in the system. blob_digest = blob_digests[layer_index] expected_status = 404 if image.urls else 200 result = self.conduct( session, "GET", "/v2/%s/blobs/%s" % (self.repo_name(namespace, repo_name), blob_digest), expected_status=(expected_status, expected_failure, V2ProtocolSteps.GET_BLOB), headers=headers, options=options, ) if expected_status == 200: assert result.content == image.bytes layer_index += 1 assert (len(blob_digests) + empty_count) >= len( images ) # OCI/Schema 2 has 1 extra for config return PullResult(manifests=manifests, image_ids=image_ids)
def push( self, session, namespace, repo_name, tag_names, images, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() scopes = options.scopes or [ "repository:%s:push,pull" % self.repo_name(namespace, repo_name) ] tag_names = [tag_names] if isinstance(tag_names, str) else tag_names # Ping! self.ping(session) # Perform auth and retrieve a token. token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None: assert V2Protocol.FAILURE_CODES[V2ProtocolSteps.AUTH].get(expected_failure) return headers = { "Authorization": "Bearer " + token, "Accept": ",".join(options.accept_mimetypes) if options.accept_mimetypes is not None else "*/*", } # Build fake manifests. manifests = {} blobs = {} for tag_name in tag_names: if self.schema == "oci": manifests[tag_name] = self.build_oci(images, blobs, options) elif self.schema == "schema2": manifests[tag_name] = self.build_schema2(images, blobs, options) elif self.schema == "schema1": manifests[tag_name] = self.build_schema1( namespace, repo_name, tag_name, images, blobs, options ) else: raise NotImplementedError(self.schema) # Push the blob data. if not self._push_blobs( blobs, session, namespace, repo_name, headers, options, expected_failure ): return # Write a manifest for each tag. for tag_name in tag_names: manifest = manifests[tag_name] # Write the manifest. If we expect it to be invalid, we expect a 404 code. Otherwise, we # expect a 201 response for success. put_code = 404 if options.manifest_invalid_blob_references else 201 manifest_headers = {"Content-Type": manifest.media_type} manifest_headers.update(headers) if options.manifest_content_type is not None: manifest_headers["Content-Type"] = options.manifest_content_type tag_or_digest = tag_name if not options.push_by_manifest_digest else manifest.digest self.conduct( session, "PUT", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), tag_or_digest), data=manifest.bytes.as_encoded_str(), expected_status=(put_code, expected_failure, V2ProtocolSteps.PUT_MANIFEST), headers=manifest_headers, ) return PushResult(manifests=manifests, headers=headers)
def push_list( self, session, namespace, repo_name, tag_names, manifestlist, manifests, blobs, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() scopes = options.scopes or [ "repository:%s:push,pull" % self.repo_name(namespace, repo_name) ] tag_names = [tag_names] if isinstance(tag_names, str) else tag_names # Ping! self.ping(session) # Perform auth and retrieve a token. token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None: assert V2Protocol.FAILURE_CODES[V2ProtocolSteps.AUTH].get(expected_failure) return headers = { "Authorization": "Bearer " + token, "Accept": ",".join(options.accept_mimetypes) if options.accept_mimetypes is not None else "*/*", } # Push all blobs. if not self._push_blobs( blobs, session, namespace, repo_name, headers, options, expected_failure ): return # Push the individual manifests. for manifest in manifests: manifest_headers = {"Content-Type": manifest.media_type} manifest_headers.update(headers) self.conduct( session, "PUT", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), manifest.digest), data=manifest.bytes.as_encoded_str(), expected_status=(201, expected_failure, V2ProtocolSteps.PUT_MANIFEST), headers=manifest_headers, ) # Push the manifest list. for tag_name in tag_names: manifest_headers = {"Content-Type": manifestlist.media_type} manifest_headers.update(headers) if options.manifest_content_type is not None: manifest_headers["Content-Type"] = options.manifest_content_type self.conduct( session, "PUT", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), tag_name), data=manifestlist.bytes.as_encoded_str(), expected_status=(201, expected_failure, V2ProtocolSteps.PUT_MANIFEST_LIST), headers=manifest_headers, ) return PushResult(manifests=None, headers=headers)
def pull_list( self, session, namespace, repo_name, tag_names, manifestlist, credentials=None, expected_failure=None, options=None, ): options = options or ProtocolOptions() scopes = options.scopes or [ "repository:%s:push,pull" % self.repo_name(namespace, repo_name) ] tag_names = [tag_names] if isinstance(tag_names, str) else tag_names # Ping! self.ping(session) # Perform auth and retrieve a token. token, _ = self.auth( session, credentials, namespace, repo_name, scopes=scopes, expected_failure=expected_failure, ) if token is None: assert V2Protocol.FAILURE_CODES[V2ProtocolSteps.AUTH].get(expected_failure) return headers = { "Authorization": "Bearer " + token, "Accept": ",".join(MANIFEST_LIST_TYPES), } for tag_name in tag_names: # Retrieve the manifest for the tag or digest. response = self.conduct( session, "GET", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), tag_name), expected_status=(200, expected_failure, V2ProtocolSteps.GET_MANIFEST_LIST), headers=headers, ) if expected_failure is not None: return None # Parse the returned manifest list and ensure it matches. ct = response.headers["Content-Type"] assert is_manifest_list_type(ct), "Expected list type, found: %s" % ct retrieved = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text), ct) assert retrieved.schema_version == 2 assert retrieved.is_manifest_list assert retrieved.digest == manifestlist.digest # Pull each of the manifests inside and ensure they can be retrieved. for manifest_digest in retrieved.child_manifest_digests(): response = self.conduct( session, "GET", "/v2/%s/manifests/%s" % (self.repo_name(namespace, repo_name), manifest_digest), expected_status=(200, expected_failure, V2ProtocolSteps.GET_MANIFEST), headers=headers, ) if expected_failure is not None: return None ct = response.headers["Content-Type"] manifest = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text), ct) assert not manifest.is_manifest_list assert manifest.digest == manifest_digest