Пример #1
0
    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
Пример #2
0
    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,
            )
Пример #3
0
    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)
Пример #4
0
    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)
Пример #5
0
    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
Пример #6
0
    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)
Пример #7
0
    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)
Пример #8
0
    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)
Пример #9
0
    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