Ejemplo n.º 1
0
def test_client_post():
    """Passes the correct method."""
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        client = Client("http://api.test", "http://storage.test")
    client.post("/somepath", "somebody")

    mock_auth().request.assert_called_once_with("POST", "http://api.test/somepath", "somebody")
Ejemplo n.º 2
0
def test_client_post():
    """Passes the correct method."""
    with patch('charmcraft.commands.store.client._AuthHolder') as mock_auth:
        client = Client()
    client.post('/somepath', 'somebody')

    mock_auth().request.assert_called_once_with('POST', API_BASE_URL + '/somepath', 'somebody')
Ejemplo n.º 3
0
def test_client_get():
    """Passes the correct method."""
    with patch('charmcraft.commands.store.client._AuthHolder') as mock_auth:
        client = Client('http://api.test', 'http://storage.test')
    client.get('/somepath')

    mock_auth().request.assert_called_once_with('GET', 'http://api.test/somepath', None)
Ejemplo n.º 4
0
def test_client_hit_url_extra_slash():
    """The configured api url is ok even with an extra slash."""
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        client = Client("https://local.test:1234/", "http://storage.test")
    client._hit("GET", "/somepath")
    mock_auth().request.assert_called_once_with(
        "GET", "https://local.test:1234/somepath", None
    )
Ejemplo n.º 5
0
def test_client_hit_success_without_json_parsing():
    """Hits the server, all ok, return the raw response without parsing the json."""
    response_value = "whatever test response"
    fake_response = FakeResponse(content=response_value, status_code=200)
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        mock_auth().request.return_value = fake_response
        client = Client("http://api.test", "http://storage.test")
    result = client._hit("GET", "/somepath", parse_json=False)

    mock_auth().request.assert_called_once_with("GET", "http://api.test/somepath", None)
    assert result == response_value
Ejemplo n.º 6
0
def test_client_hit_failure():
    """Hits the server, got a failure."""
    response_value = "raw data"
    fake_response = FakeResponse(content=response_value, status_code=404)
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        mock_auth().request.return_value = fake_response
        client = Client("http://api.test", "http://storage.test")

    expected = r"Failure working with the Store: \[404\] 'raw data'"
    with pytest.raises(CommandError, match=expected):
        client._hit("GET", "/somepath")
Ejemplo n.º 7
0
def test_client_errorparsing_nojson():
    """Produce a default message if response is not a json."""
    response = FakeResponse(content="this is not a json", status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Failure working with the Store: [404] 'this is not a json'"
Ejemplo n.º 8
0
def test_client_errorparsing_no_errors_inside():
    """Produce a default message if response has no errors list."""
    content = json.dumps({"another-error-key": "stuff"})
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Failure working with the Store: [404] " + repr(content)
Ejemplo n.º 9
0
def test_client_errorparsing_empty_errors():
    """Produce a default message if error list is empty."""
    content = json.dumps({"error-list": []})
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Failure working with the Store: [404] " + repr(content)
Ejemplo n.º 10
0
def test_client_errorparsing_bad_structure():
    """Produce a default message if error list has a bad format."""
    content = json.dumps({"error-list": ["whatever"]})
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Failure working with the Store: [404] " + repr(content)
Ejemplo n.º 11
0
def test_client_errorparsing_no_code():
    """Build the error message using original message (even when code in None)."""
    content = json.dumps({"error-list": [{"message": "error message", "code": None}]})
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Store failure! error message"
Ejemplo n.º 12
0
def test_client_hit_success_withbody(caplog):
    """Hits the server including a body, all ok."""
    caplog.set_level(logging.DEBUG, logger="charmcraft.commands")

    response_value = {"foo": "bar"}
    fake_response = FakeResponse(content=json.dumps(response_value), status_code=200)
    with patch('charmcraft.commands.store.client._AuthHolder') as mock_auth:
        mock_auth().request.return_value = fake_response
        client = Client()
    result = client._hit('POST', '/somepath', 'somebody')

    mock_auth().request.assert_called_once_with('POST', API_BASE_URL + '/somepath', 'somebody')
    assert result == response_value
    expected = [
        "Hitting the store: POST {}/somepath somebody".format(API_BASE_URL),
        "Store ok: 200",
    ]
    assert expected == [rec.message for rec in caplog.records]
Ejemplo n.º 13
0
def test_client_hit_success_simple(caplog):
    """Hits the server, all ok."""
    caplog.set_level(logging.DEBUG, logger="charmcraft.commands")

    response_value = {"foo": "bar"}
    fake_response = FakeResponse(content=json.dumps(response_value), status_code=200)
    with patch('charmcraft.commands.store.client._AuthHolder') as mock_auth:
        mock_auth().request.return_value = fake_response
        client = Client('http://api.test', 'http://storage.test')
    result = client._hit('GET', '/somepath')

    mock_auth().request.assert_called_once_with('GET', 'http://api.test/somepath', None)
    assert result == response_value
    expected = [
        "Hitting the store: GET http://api.test/somepath None",
        "Store ok: 200",
    ]
    assert expected == [rec.message for rec in caplog.records]
Ejemplo n.º 14
0
def test_client_errorparsing_multiple():
    """Build the error message coumpounding the different received ones."""
    content = json.dumps({"error-list": [
        {'message': 'error 1', 'code': 'test-error-1'},
        {'message': 'error 2', 'code': None},
    ]})
    response = FakeResponse(content=content, status_code=404)
    result = Client()._parse_store_error(response)
    assert result == "Store failure! error 1 [code: test-error-1]; error 2"
Ejemplo n.º 15
0
def test_client_hit_success_withbody(caplog):
    """Hits the server including a body, all ok."""
    caplog.set_level(logging.DEBUG, logger="charmcraft.commands")

    response_value = {"foo": "bar"}
    fake_response = FakeResponse(content=json.dumps(response_value), status_code=200)
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        mock_auth().request.return_value = fake_response
        client = Client("http://api.test", "http://storage.test")
    result = client._hit("POST", "/somepath", "somebody")

    mock_auth().request.assert_called_once_with("POST", "http://api.test/somepath", "somebody")
    assert result == response_value
    expected = [
        "Hitting the store: POST http://api.test/somepath somebody",
        "Store ok: 200",
    ]
    assert expected == [rec.message for rec in caplog.records]
Ejemplo n.º 16
0
def test_client_errorparsing_complete():
    """Build the error message using original message and code."""
    content = json.dumps(
        {"error-list": [{"message": "error message", "code": "test-error"}]}
    )
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(
        response
    )
    assert result == "Store failure! error message [code: test-error]"
def test_client_errorparsing_complete():
    """Build the error message using original message and code."""
    content = json.dumps(
        {"error-list": [{
            'message': 'error message',
            'code': 'test-error'
        }]})
    response = FakeResponse(content=content, status_code=404)
    result = Client()._parse_store_error(response)
    assert result == "Store failure! error message [code: test-error]"
Ejemplo n.º 18
0
def test_client_push_response_not_ok(tmp_path):
    """Didn't get a 200 from the Storage."""
    # fake some bytes to push
    test_filepath = tmp_path / "supercharm.bin"
    with test_filepath.open("wb") as fh:
        fh.write(b"abcdefgh")

    with patch("charmcraft.commands.store.client._storage_push") as mock:
        mock.return_value = FakeResponse(content="had a problem", status_code=500)
        with pytest.raises(CommandError) as cm:
            Client("http://api.test", "http://storage.test").push(test_filepath)
        assert str(cm.value) == "Failure while pushing file: [500] 'had a problem'"
Ejemplo n.º 19
0
def test_client_push_configured_url_extra_slash(caplog, tmp_path, capsys):
    """The configured storage url is ok even with an extra slash."""
    def fake_pusher(monitor, storage_base_url):
        """Check the received URL."""
        assert storage_base_url == "https://local.test:1234"

        content = json.dumps(dict(successful=True, upload_id='test-upload-id'))
        return FakeResponse(content=content, status_code=200)

    test_filepath = tmp_path / 'supercharm.bin'
    test_filepath.write_text("abcdefgh")
    with patch('charmcraft.commands.store.client._storage_push', fake_pusher):
        Client('http://api.test', 'https://local.test:1234/').push(test_filepath)
Ejemplo n.º 20
0
def test_client_errorparsing_multiple():
    """Build the error message coumpounding the different received ones."""
    content = json.dumps(
        {
            "error-list": [
                {"message": "error 1", "code": "test-error-1"},
                {"message": "error 2", "code": None},
            ]
        }
    )
    response = FakeResponse(content=content, status_code=404)
    result = Client("http://api.test", "http://storage.test")._parse_store_error(response)
    assert result == "Store failure! error 1 [code: test-error-1]; error 2"
Ejemplo n.º 21
0
def test_client_push_configured_url_simple(tmp_path, capsys):
    """The storage server can be configured."""

    def fake_pusher(monitor, storage_base_url):
        """Check the received URL."""
        assert storage_base_url == "https://local.test:1234"

        content = json.dumps(dict(successful=True, upload_id="test-upload-id"))
        return FakeResponse(content=content, status_code=200)

    test_filepath = tmp_path / "supercharm.bin"
    test_filepath.write_text("abcdefgh")
    with patch("charmcraft.commands.store.client._storage_push", fake_pusher):
        Client("http://api.test", "https://local.test:1234/").push(test_filepath)
Ejemplo n.º 22
0
def test_client_push_response_unsuccessful(tmp_path):
    """Didn't get a 200 from the Storage."""
    # fake some bytes to push
    test_filepath = tmp_path / "supercharm.bin"
    with test_filepath.open("wb") as fh:
        fh.write(b"abcdefgh")

    with patch("charmcraft.commands.store.client._storage_push") as mock:
        raw_content = dict(successful=False, upload_id=None)
        mock.return_value = FakeResponse(content=json.dumps(raw_content), status_code=200)
        with pytest.raises(CommandError) as cm:
            Client("http://api.test", "http://storage.test").push(test_filepath)
        expected = "Server error while pushing file: {'successful': False, 'upload_id': None}"
        assert str(cm.value) == expected
def test_client_push_response_unsuccessful(tmp_path):
    """Didn't get a 200 from the Storage."""
    # fake some bytes to push
    test_filepath = tmp_path / 'supercharm.bin'
    with test_filepath.open('wb') as fh:
        fh.write(b"abcdefgh")

    with patch('charmcraft.commands.store.client._storage_push') as mock:
        raw_content = dict(successful=False, upload_id=None)
        mock.return_value = FakeResponse(content=json.dumps(raw_content),
                                         status_code=200)
        with pytest.raises(CommandError) as cm:
            Client().push(test_filepath)
        assert str(cm.value
                   ) == "Server error while pushing file: " + repr(raw_content)
Ejemplo n.º 24
0
def test_client_init():
    """Check how craft-store's Client is initiated."""
    api_url = "http://api.test"
    storage_url = "http://storage.test"
    user_agent = "Super User Agent"
    with patch("craft_store.StoreClient.__init__") as mock_client_init:
        with patch("charmcraft.commands.store.client.build_user_agent") as mock_ua:
            mock_ua.return_value = user_agent
            Client(api_url, storage_url)
    mock_client_init.assert_called_with(
        base_url=api_url,
        endpoints=craft_store.endpoints.CHARMHUB,
        application_name="charmcraft",
        user_agent=user_agent,
        environment_auth="CHARMCRAFT_AUTH",
    )
Ejemplo n.º 25
0
def test_client_push_response_unsuccessful(tmp_path):
    """Didn't get a 200 from the Storage."""
    # fake some bytes to push
    test_filepath = tmp_path / 'supercharm.bin'
    with test_filepath.open('wb') as fh:
        fh.write(b"abcdefgh")

    with patch('charmcraft.commands.store.client._storage_push') as mock:
        raw_content = dict(successful=False, upload_id=None)
        mock.return_value = FakeResponse(content=json.dumps(raw_content), status_code=200)
        with pytest.raises(CommandError) as cm:
            Client().push(test_filepath)
        # checking all this separatedly as in Py3.5 dicts order is not deterministic
        message = str(cm.value)
        assert "Server error while pushing file:" in message
        assert "'successful': False" in message
        assert "'upload_id': None" in message
Ejemplo n.º 26
0
def test_client_push_simple_ok(caplog, tmp_path, capsys):
    """Happy path for pushing bytes."""
    caplog.set_level(logging.DEBUG, logger="charmcraft.commands")

    # fake some bytes to push
    test_filepath = tmp_path / "supercharm.bin"
    with test_filepath.open("wb") as fh:
        fh.write(b"abcdefgh")

    def fake_pusher(monitor, storage_base_url):
        """Push bytes in sequence, doing verifications in the middle."""
        assert storage_base_url == "http://storage.test"

        total_to_push = (
            monitor.len
        )  # not only the saved bytes, but also headers and stuff

        # one batch
        monitor.read(20)
        captured = capsys.readouterr()
        assert captured.out == "Uploading... {:.2f}%\r".format(100 * 20 / total_to_push)

        # another batch
        monitor.read(20)
        captured = capsys.readouterr()
        assert captured.out == "Uploading... {:.2f}%\r".format(100 * 40 / total_to_push)

        # check monitor is properly built
        assert isinstance(monitor.encoder, MultipartEncoder)
        filename, fh, ctype = monitor.encoder.fields["binary"]
        assert filename == "supercharm.bin"
        assert fh.name == str(test_filepath)
        assert ctype == "application/octet-stream"

        content = json.dumps(dict(successful=True, upload_id="test-upload-id"))
        return FakeResponse(content=content, status_code=200)

    with patch("charmcraft.commands.store.client._storage_push", fake_pusher):
        Client("http://api.test", "http://storage.test").push(test_filepath)

    # check proper logs
    expected = [
        "Starting to push {}".format(str(test_filepath)),
        "Uploading bytes ended, id test-upload-id",
    ]
    assert expected == [rec.message for rec in caplog.records]
Ejemplo n.º 27
0
 def __init__(self, charmhub_config):
     self._client = Client(charmhub_config.api_url,
                           charmhub_config.storage_url)
Ejemplo n.º 28
0
class Store:
    """The main interface to the Store's API."""

    def __init__(self, charmhub_config):
        self._client = Client(charmhub_config.api_url, charmhub_config.storage_url)

    def login(self):
        """Login into the store.

        The login happens on every request to the Store (if current credentials were not
        enough), so to trigger a new login we...

            - remove local credentials

            - exercise the simplest command regarding developer identity
        """
        self._client.clear_credentials()
        self._client.get("/v1/whoami")

    def logout(self):
        """Logout from the store.

        There's no action really in the Store to logout, we just remove local credentials.
        """
        self._client.clear_credentials()

    def whoami(self):
        """Return authenticated user details."""
        response = self._client.get("/v1/whoami")
        # XXX Facundo 2020-06-30: Every time we consume data from the Store (after a succesful
        # call) we need to wrap it with a context manager that will raise UnknownError (after
        # logging in debug the received response). This would catch API changes, for example,
        # without making charmcraft to badly crash. Related: issue #73.
        result = User(
            name=response["display-name"],
            username=response["username"],
            userid=response["id"],
        )
        return result

    def register_name(self, name, entity_type):
        """Register the specified name for the authenticated user."""
        self._client.post("/v1/charm", {"name": name, "type": entity_type})

    def list_registered_names(self):
        """Return names registered by the authenticated user."""
        response = self._client.get("/v1/charm")
        result = []
        for item in response["results"]:
            result.append(
                Entity(
                    name=item["name"],
                    private=item["private"],
                    status=item["status"],
                    entity_type=item["type"],
                )
            )
        return result

    def _upload(self, endpoint, filepath, *, extra_fields=None):
        """Upload for all charms, bundles and resources (generic process)."""
        upload_id = self._client.push(filepath)
        payload = {"upload-id": upload_id}
        if extra_fields is not None:
            payload.update(extra_fields)
        response = self._client.post(endpoint, payload)
        status_url = response["status-url"]
        logger.debug("Upload %s started, got status url %s", upload_id, status_url)

        while True:
            response = self._client.get(status_url)
            logger.debug("Status checked: %s", response)

            # as we're asking for a single upload_id, the response will always have only one item
            (revision,) = response["revisions"]
            status = revision["status"]

            if status in UPLOAD_ENDING_STATUSES:
                return Uploaded(
                    ok=UPLOAD_ENDING_STATUSES[status],
                    errors=_build_errors(revision),
                    status=status,
                    revision=revision["revision"],
                )

            # XXX Facundo 2020-06-30: Implement a slight backoff algorithm and fallout after
            # N attempts (which should be big, as per snapcraft experience). Issue: #79.
            time.sleep(POLL_DELAY)

    def upload(self, name, filepath):
        """Upload the content of filepath to the indicated charm."""
        endpoint = "/v1/charm/{}/revisions".format(name)
        return self._upload(endpoint, filepath)

    def upload_resource(self, charm_name, resource_name, resource_type, filepath):
        """Upload the content of filepath to the indicated resource."""
        endpoint = "/v1/charm/{}/resources/{}/revisions".format(charm_name, resource_name)
        return self._upload(endpoint, filepath, extra_fields={"type": resource_type})

    def list_revisions(self, name):
        """Return charm revisions for the indicated charm."""
        response = self._client.get("/v1/charm/{}/revisions".format(name))
        result = [_build_revision(item) for item in response["revisions"]]
        return result

    def release(self, name, revision, channels, resources):
        """Release one or more revisions for a package."""
        endpoint = "/v1/charm/{}/releases".format(name)
        resources = [{"name": res.name, "revision": res.revision} for res in resources]
        items = [
            {"revision": revision, "channel": channel, "resources": resources}
            for channel in channels
        ]
        self._client.post(endpoint, items)

    def list_releases(self, name):
        """List current releases for a package."""
        endpoint = "/v1/charm/{}/releases".format(name)
        response = self._client.get(endpoint)

        channel_map = []
        for item in response["channel-map"]:
            expires_at = item["expiration-date"]
            if expires_at is not None:
                # `datetime.datetime.fromisoformat` is available only since Py3.7
                expires_at = parser.parse(expires_at)
            resources = [_build_resource(r) for r in item["resources"]]
            channel_map.append(
                Release(
                    revision=item["revision"],
                    channel=item["channel"],
                    expires_at=expires_at,
                    resources=resources,
                    base=Base(**item["base"]),
                )
            )

        channels = [
            Channel(
                name=item["name"],
                fallback=item["fallback"],
                track=item["track"],
                risk=item["risk"],
                branch=item["branch"],
            )
            for item in response["package"]["channels"]
        ]

        revisions = [_build_revision(item) for item in response["revisions"]]

        return channel_map, channels, revisions

    def create_library_id(self, charm_name, lib_name):
        """Create a new library id."""
        endpoint = "/v1/charm/libraries/{}".format(charm_name)
        response = self._client.post(endpoint, {"library-name": lib_name})
        lib_id = response["library-id"]
        return lib_id

    def create_library_revision(self, charm_name, lib_id, api, patch, content, content_hash):
        """Create a new library revision."""
        endpoint = "/v1/charm/libraries/{}/{}".format(charm_name, lib_id)
        payload = {
            "api": api,
            "patch": patch,
            "content": content,
            "hash": content_hash,
        }
        response = self._client.post(endpoint, payload)
        result = _build_library(response)
        return result

    def get_library(self, charm_name, lib_id, api):
        """Get the library tip by id for a given api version."""
        endpoint = "/v1/charm/libraries/{}/{}?api={}".format(charm_name, lib_id, api)
        response = self._client.get(endpoint)
        result = _build_library(response)
        return result

    def get_libraries_tips(self, libraries):
        """Get the tip details for several libraries at once.

        Each requested library can be specified in different ways: using the library id
        or the charm and library names (both will pinpoint the library), but in the later
        case the library name is optional (so all libraries for that charm will be
        returned). Also, for all those cases, an API version can be specified.
        """
        endpoint = "/v1/charm/libraries/bulk"
        payload = []
        for lib in libraries:
            if "lib_id" in lib:
                item = {
                    "library-id": lib["lib_id"],
                }
            else:
                item = {
                    "charm-name": lib["charm_name"],
                }
                if "lib_name" in lib:
                    item["library-name"] = lib["lib_name"]
            if "api" in lib:
                item["api"] = lib["api"]
            payload.append(item)
        response = self._client.post(endpoint, payload)
        libraries = response["libraries"]
        result = {(item["library-id"], item["api"]): _build_library(item) for item in libraries}
        return result

    def list_resources(self, charm):
        """Return resources associated to the indicated charm."""
        response = self._client.get("/v1/charm/{}/resources".format(charm))
        result = [_build_resource(item) for item in response["resources"]]
        return result

    def list_resource_revisions(self, charm_name, resource_name):
        """Return revisions for the indicated charm resource."""
        endpoint = "/v1/charm/{}/resources/{}/revisions".format(charm_name, resource_name)
        response = self._client.get(endpoint)
        result = [_build_resource_revision(item) for item in response["revisions"]]
        return result

    def get_oci_registry_credentials(self, charm_name, resource_name):
        """Get credentials to upload a resource to the Canonical's OCI Registry."""
        endpoint = "/v1/charm/{}/resources/{}/oci-image/upload-credentials".format(
            charm_name, resource_name
        )
        response = self._client.get(endpoint)
        return RegistryCredentials(
            image_name=response["image-name"],
            username=response["username"],
            password=response["password"],
        )

    def get_oci_image_blob(self, charm_name, resource_name, digest):
        """Get the blob that points to the OCI image in the Canonical's OCI Registry."""
        payload = {"image-digest": digest}
        endpoint = "/v1/charm/{}/resources/{}/oci-image/blob".format(charm_name, resource_name)
        content = self._client.post(endpoint, payload, parse_json=False)
        # the response here is returned as is, because it's opaque to charmcraft
        return content
Ejemplo n.º 29
0
class Store:
    """The main interface to the Store's API."""
    def __init__(self, charmhub_config):
        self._client = Client(charmhub_config.api_url,
                              charmhub_config.storage_url)

    def login(self):
        """Login into the store.

        The login happens on every request to the Store (if current credentials were not
        enough), so to trigger a new login we...

            - remove local credentials

            - exercise the simplest command regarding developer identity
        """
        self._client.clear_credentials()
        self._client.get('/v1/whoami')

    def logout(self):
        """Logout from the store.

        There's no action really in the Store to logout, we just remove local credentials.
        """
        self._client.clear_credentials()

    def whoami(self):
        """Return authenticated user details."""
        response = self._client.get('/v1/whoami')
        # XXX Facundo 2020-06-30: Every time we consume data from the Store (after a succesful
        # call) we need to wrap it with a context manager that will raise UnknownError (after
        # logging in debug the received response). This would catch API changes, for example,
        # without making charmcraft to badly crash. Related: issue #73.
        result = User(
            name=response['display-name'],
            username=response['username'],
            userid=response['id'],
        )
        return result

    def register_name(self, name, entity_type):
        """Register the specified name for the authenticated user."""
        self._client.post('/v1/charm', {'name': name, 'type': entity_type})

    def list_registered_names(self):
        """Return names registered by the authenticated user."""
        response = self._client.get('/v1/charm')
        result = []
        for item in response['results']:
            result.append(
                Entity(name=item['name'],
                       private=item['private'],
                       status=item['status'],
                       entity_type=item['type']))
        return result

    def _upload(self, endpoint, filepath):
        """Upload for all charms, bundles and resources (generic process)."""
        upload_id = self._client.push(filepath)
        response = self._client.post(endpoint, {'upload-id': upload_id})
        status_url = response['status-url']
        logger.debug("Upload %s started, got status url %s", upload_id,
                     status_url)

        while True:
            response = self._client.get(status_url)
            logger.debug("Status checked: %s", response)

            # as we're asking for a single upload_id, the response will always have only one item
            (revision, ) = response['revisions']
            status = revision['status']

            if status in UPLOAD_ENDING_STATUSES:
                return Uploaded(ok=UPLOAD_ENDING_STATUSES[status],
                                errors=_build_errors(revision),
                                status=status,
                                revision=revision['revision'])

            # XXX Facundo 2020-06-30: Implement a slight backoff algorithm and fallout after
            # N attempts (which should be big, as per snapcraft experience). Issue: #79.
            time.sleep(POLL_DELAY)

    def upload(self, name, filepath):
        """Upload the content of filepath to the indicated charm."""
        endpoint = '/v1/charm/{}/revisions'.format(name)
        return self._upload(endpoint, filepath)

    def upload_resource(self, charm_name, resource_name, filepath):
        """Upload the content of filepath to the indicated resource."""
        endpoint = '/v1/charm/{}/resources/{}/revisions'.format(
            charm_name, resource_name)
        return self._upload(endpoint, filepath)

    def list_revisions(self, name):
        """Return charm revisions for the indicated charm."""
        response = self._client.get('/v1/charm/{}/revisions'.format(name))
        result = [_build_revision(item) for item in response['revisions']]
        return result

    def release(self, name, revision, channels, resources):
        """Release one or more revisions for a package."""
        endpoint = '/v1/charm/{}/releases'.format(name)
        resources = [{
            'name': res.name,
            'revision': res.revision
        } for res in resources]
        items = [{
            'revision': revision,
            'channel': channel,
            'resources': resources
        } for channel in channels]
        self._client.post(endpoint, items)

    def list_releases(self, name):
        """List current releases for a package."""
        endpoint = '/v1/charm/{}/releases'.format(name)
        response = self._client.get(endpoint)

        channel_map = []
        for item in response['channel-map']:
            expires_at = item['expiration-date']
            if expires_at is not None:
                # `datetime.datetime.fromisoformat` is available only since Py3.7
                expires_at = parser.parse(expires_at)
            channel_map.append(
                Release(revision=item['revision'],
                        channel=item['channel'],
                        expires_at=expires_at))

        channels = [
            Channel(
                name=item['name'],
                fallback=item['fallback'],
                track=item['track'],
                risk=item['risk'],
                branch=item['branch'],
            ) for item in response['package']['channels']
        ]

        revisions = [_build_revision(item) for item in response['revisions']]

        return channel_map, channels, revisions

    def create_library_id(self, charm_name, lib_name):
        """Create a new library id."""
        endpoint = '/v1/charm/libraries/{}'.format(charm_name)
        response = self._client.post(endpoint, {'library-name': lib_name})
        lib_id = response['library-id']
        return lib_id

    def create_library_revision(self, charm_name, lib_id, api, patch, content,
                                content_hash):
        """Create a new library revision."""
        endpoint = '/v1/charm/libraries/{}/{}'.format(charm_name, lib_id)
        payload = {
            'api': api,
            'patch': patch,
            'content': content,
            'hash': content_hash,
        }
        response = self._client.post(endpoint, payload)
        result = _build_library(response)
        return result

    def get_library(self, charm_name, lib_id, api):
        """Get the library tip by id for a given api version."""
        endpoint = '/v1/charm/libraries/{}/{}?api={}'.format(
            charm_name, lib_id, api)
        response = self._client.get(endpoint)
        result = _build_library(response)
        return result

    def get_libraries_tips(self, libraries):
        """Get the tip details for several libraries at once.

        Each requested library can be specified in different ways: using the library id
        or the charm and library names (both will pinpoint the library), but in the later
        case the library name is optional (so all libraries for that charm will be
        returned). Also, for all those cases, an API version can be specified.
        """
        endpoint = '/v1/charm/libraries/bulk'
        payload = []
        for lib in libraries:
            if 'lib_id' in lib:
                item = {
                    'library-id': lib['lib_id'],
                }
            else:
                item = {
                    'charm-name': lib['charm_name'],
                }
                if 'lib_name' in lib:
                    item['library-name'] = lib['lib_name']
            if 'api' in lib:
                item['api'] = lib['api']
            payload.append(item)
        response = self._client.post(endpoint, payload)
        libraries = response['libraries']
        result = {(item['library-id'], item['api']): _build_library(item)
                  for item in libraries}
        return result

    def list_resources(self, charm):
        """Return resources associated to the indicated charm."""
        response = self._client.get('/v1/charm/{}/resources'.format(charm))
        result = [_build_resource(item) for item in response['resources']]
        return result

    def list_resource_revisions(self, charm_name, resource_name):
        """Return revisions for the indicated charm resource."""
        endpoint = '/v1/charm/{}/resources/{}/revisions'.format(
            charm_name, resource_name)
        response = self._client.get(endpoint)
        result = [
            _build_resource_revision(item) for item in response['revisions']
        ]
        return result
Ejemplo n.º 30
0
def test_client_clear_credentials():
    with patch("charmcraft.commands.store.client._AuthHolder") as mock_auth:
        client = Client("http://api.test", "http://storage.test")
    client.clear_credentials()

    mock_auth().clear_credentials.assert_called_once_with()