← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] ~pelpsi/launchpad:fetch-service-session-API into launchpad:master

 

Good work - left a few comments!
I'll leave the approval to Jürgen who is more into the details of the whole project.

Diff comments:

> diff --git a/lib/lp/buildmaster/builderproxy.py b/lib/lp/buildmaster/builderproxy.py
> index be65e29..4f249a7 100644
> --- a/lib/lp/buildmaster/builderproxy.py
> +++ b/lib/lp/buildmaster/builderproxy.py
> @@ -52,6 +62,25 @@ class BuilderProxyMixin:
>                  endpoint=_get_proxy_config("builder_proxy_auth_api_endpoint"),
>                  token=token["username"],
>              )
> +        if (

This should be an `elif` so that it doesn't try to check the second `if statement if the first is true.

Also, how about doing:
```
if not allow_internet:
    return

if fetch_service and _get_proxy_config("fetch_service_host"):
    ...
elif not fetch_service and _get_proxy_config("builder_proxy_host"):
    ...
```

Seems a bit easier to follow IMO

> +            fetch_service
> +            and _get_proxy_config("fetch_service_host")
> +            and allow_internet
> +        ):
> +            # http://<session-id>:<secret>@<addr>:9988/

Can you be more clear with what you mean with the comment? Is this the expected proxy_url format? If so, I see the string format bellow already hints the format quite well, so perhaps this is not needed

> +            token = yield self._requestFetchServiceSession()
> +            args["proxy_url"] = (
> +                "http://{session_id}:{token}@{host}:{port}".format(
> +                    session_id=token["id"],
> +                    token=token["token"],
> +                    host=_get_proxy_config("fetch_service_host"),
> +                    port=_get_proxy_config("fetch_service_port"),
> +                )
> +            )
> +            args["revocation_endpoint"] = "{endpoint}/session/{id}".format(
> +                endpoint=_get_proxy_config("fetch_service_auth_api_endpoint"),
> +                id=token["id"],
> +            )
>  
>      @defer.inlineCallbacks
>      def _requestProxyToken(self):
> @@ -86,3 +115,39 @@ class BuilderProxyMixin:
>              proxy_username=proxy_username,
>          )
>          return token
> +
> +    @defer.inlineCallbacks
> +    def _requestFetchServiceSession(self):
> +        # This is a different function if we have the needs to

Not sure what you mean in this comment, can you clarify?

> +        # differentiate more the behavior.
> +        admin_username = _get_proxy_config(
> +            "fetch_service_auth_api_admin_username"
> +        )
> +        if not admin_username:

Since this pattern is used so much in this class here and by the builder proxy route (checking a variable and raising), perhaps we could have a method to check values are in config and raise separately?

Something like:
```
def get_value_from_config(self, key: string):
    value = _get_proxy_config(key)
    if not value:
        raise CannotBuild(f"'{key}' is not configured")
    return value
```

Which could then be called as:
```
admin_username = self.get_value_from_config("fetch_service_auth_api_admin_username")
```

Just a thought, this is also fine as is.

> +            raise CannotBuild(
> +                "fetch_service_auth_api_admin_username is not configured."
> +            )
> +        secret = _get_proxy_config("fetch_service_auth_api_admin_secret")
> +        if not secret:
> +            raise CannotBuild(
> +                "fetch_service_auth_api_admin_secret is not configured."
> +            )
> +        url = _get_proxy_config("fetch_service_auth_api_endpoint")
> +        if not secret:
> +            raise CannotBuild(
> +                "fetch_service_auth_api_endpoint is not configured."
> +            )
> +        timestamp = int(time.time())
> +        proxy_username = "{build_id}-{timestamp}".format(
> +            build_id=self.build.build_cookie, timestamp=timestamp
> +        )
> +        auth_string = f"{admin_username}:{secret}".strip()
> +        auth_header = b"Basic " + base64.b64encode(auth_string.encode("ASCII"))
> +
> +        token = yield self._worker.process_pool.doWork(
> +            RequestFetchServiceSessionCommand,
> +            url=url,
> +            auth_header=auth_header,
> +            proxy_username=proxy_username,
> +        )
> +        return token
> diff --git a/lib/lp/buildmaster/downloader.py b/lib/lp/buildmaster/downloader.py
> index fc16852..cee308a 100644
> --- a/lib/lp/buildmaster/downloader.py
> +++ b/lib/lp/buildmaster/downloader.py
> @@ -94,3 +110,24 @@ class RequestProcess(AMPChild):
>              )
>              response.raise_for_status()
>              return response.json()
> +
> +    @RequestFetchServiceSessionCommand.responder
> +    def requestFetchServiceSessionCommand(
> +        self, url, auth_header, proxy_username
> +    ):
> +        with Session() as session:
> +            session.trust_env = False
> +            # XXX pelpsi 2024-02-28: should take the same

I'm not sure I understand what's missing by this comment, can you clarify?

> +            # json body? From ST108 we should provide
> +            # {
> +            #   "timeout": <int>,			// session timeout in seconds
> +            #   "policy": <string>			// "strict" or "permissive"
> +            # }
> +            response = session.post(
> +                url,
> +                headers={"Authorization": auth_header},
> +                json={"username": proxy_username},
> +            )
> +            print(" CI SIAMO???? ")

Extra comment left here :)

> +            response.raise_for_status()
> +            return response.json()
> diff --git a/lib/lp/buildmaster/tests/fetchservice.py b/lib/lp/buildmaster/tests/fetchservice.py
> new file mode 100644
> index 0000000..9a12933
> --- /dev/null
> +++ b/lib/lp/buildmaster/tests/fetchservice.py
> @@ -0,0 +1,119 @@
> +# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the

s/2015-2019/2024

> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Fixtures for dealing with the build time HTTP proxy."""
> +
> +import json
> +import uuid
> +from textwrap import dedent
> +from urllib.parse import urlsplit
> +
> +import fixtures
> +from testtools.matchers import Equals, HasLength, MatchesStructure
> +from twisted.internet import defer, endpoints, reactor
> +from twisted.python.compat import nativeString
> +from twisted.web import resource, server
> +
> +from lp.services.config import config
> +
> +
> +class FetchServiceAuthAPITokensResource(resource.Resource):
> +    """A test tokens resource for the proxy authentication API."""
> +
> +    isLeaf = True
> +
> +    def __init__(self):
> +        resource.Resource.__init__(self)
> +        self.requests = []
> +
> +    def render_POST(self, request):
> +        content = json.loads(request.content.read().decode("UTF-8"))
> +        self.requests.append(
> +            {
> +                "method": request.method,
> +                "uri": request.uri,
> +                "headers": dict(request.requestHeaders.getAllRawHeaders()),
> +                "json": content,
> +            }
> +        )
> +        return json.dumps(
> +            {
> +                "id": "1",
> +                "token": uuid.uuid4().hex,
> +            }
> +        ).encode("UTF-8")
> +
> +
> +class InProcessFetchServiceAuthAPIFixture(fixtures.Fixture):
> +    """A fixture that pretends to be the proxy authentication API.
> +
> +    Users of this fixture must call the `start` method, which returns a
> +    `Deferred`, and arrange for that to get back to the reactor.  This is
> +    necessary because the basic fixture API does not allow `setUp` to return
> +    anything.  For example:
> +
> +        class TestSomething(TestCase):
> +
> +            run_tests_with = AsynchronousDeferredRunTest.make_factory(
> +                timeout=30)
> +
> +            @defer.inlineCallbacks
> +            def setUp(self):
> +                super().setUp()
> +                yield self.useFixture(
> +                    InProcessFetchServiceAuthAPIFixture()
> +                ).start()
> +    """
> +
> +    @defer.inlineCallbacks
> +    def start(self):
> +        root = resource.Resource()
> +        self.tokens = FetchServiceAuthAPITokensResource()
> +        root.putChild(b"tokens", self.tokens)
> +        endpoint = endpoints.serverFromString(reactor, nativeString("tcp:0"))
> +        site = server.Site(self.tokens)
> +        self.addCleanup(site.stopFactory)
> +        port = yield endpoint.listen(site)
> +        self.addCleanup(port.stopListening)
> +        config.push(
> +            "in-process-fetch-service-api-fixture",
> +            dedent(
> +                """
> +                [builddmaster]
> +                fetch_service_auth_api_admin_secret: admin-secret
> +                fetch_service_auth_api_admin_username: admin-launchpad.test
> +                fetch_service_auth_api_endpoint: http://{host}:{port}/tokens
> +                fetch_service_host: {host}
> +                fetch_service_port: {port}
> +                """
> +            ).format(host=port.getHost().host, port=port.getHost().port),
> +        )
> +        self.addCleanup(config.pop, "in-process-fetch-service-api-fixture")
> +
> +
> +class FetchServiceURLMatcher(MatchesStructure):
> +    """Check that a string is a valid url for a builder proxy."""
> +
> +    def __init__(self):
> +        super().__init__(
> +            scheme=Equals("http"),
> +            id=Equals(1),
> +            token=HasLength(32),
> +            hostname=Equals(config.builddmaster.fetch_service_host),
> +            port=Equals(config.builddmaster.fetch_service_port),
> +            path=Equals(""),
> +        )
> +
> +    def match(self, matchee):
> +        super().match(urlsplit(matchee))
> +
> +
> +class RevocationEndpointMatcher(Equals):
> +    """Check that a string is a valid endpoint for proxy token revocation."""
> +
> +    def __init__(self, session_id):
> +        super().__init__(
> +            "{}/session/{}".format(

Add variable names to make it easier to read?

> +                config.builddmaster.fetch_service_auth_api_endpoint, session_id
> +            )
> +        )
> diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
> index 5ec5804..45ee8c8 100644
> --- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py
> +++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
> @@ -296,6 +317,85 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
>          )
>  
>      @defer.inlineCallbacks
> +    def test_requestFetchServiceSession_unconfigured(self):
> +        self.pushConfig("builddmaster", fetch_service_host=None)
> +        self.useFixture(
> +            FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"})

Is t worth it to test that if the feature flag is OFF, it should defaut to the builder proxy? When the feature flag is off, the `snap.use_fetch_service` should return None so it should use the builder proxy, is that right?

> +        )
> +        snap = self.factory.makeSnap(use_fetch_service=True)
> +        request = self.factory.makeSnapBuildRequest(snap=snap)
> +        job = self.makeJob(snap=snap, build_request=request)
> +        with dbuser(config.builddmaster.dbuser):
> +            args = yield job.extraBuildArgs()
> +        self.assertEqual([], self.fetch_service_api.tokens.requests)
> +        self.assertNotIn("proxy_url", args)
> +        self.assertNotIn("revocation_endpoint", args)
> +
> +    @defer.inlineCallbacks
> +    def test_requestFetchServiceSession_no_secret(self):
> +        self.pushConfig(
> +            "builddmaster", fetch_service_auth_api_admin_secret=None
> +        )
> +        self.useFixture(
> +            FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"})
> +        )
> +        snap = self.factory.makeSnap(use_fetch_service=True)
> +        request = self.factory.makeSnapBuildRequest(snap=snap)
> +        job = self.makeJob(snap=snap, build_request=request)
> +        expected_exception_msg = (
> +            "fetch_service_auth_api_admin_secret is not configured."
> +        )
> +        with ExpectedException(CannotBuild, expected_exception_msg):
> +            yield job.extraBuildArgs()
> +
> +    @defer.inlineCallbacks
> +    def test_requestFetchServiceSession(self):
> +        self.useFixture(
> +            FeatureFixture({SNAP_USE_FETCH_SERVICE_FEATURE_FLAG: "on"})
> +        )
> +        snap = self.factory.makeSnap(use_fetch_service=True)
> +        request = self.factory.makeSnapBuildRequest(snap=snap)
> +        job = self.makeJob(snap=snap, build_request=request)
> +        yield job.extraBuildArgs()
> +        expected_uri = urlsplit(
> +            config.builddmaster.fetch_service_auth_api_endpoint
> +        ).path.encode("UTF-8")
> +        request_matcher = MatchesDict(
> +            {
> +                "method": Equals(b"POST"),
> +                "uri": Equals(expected_uri),
> +                "headers": ContainsDict(
> +                    {
> +                        b"Authorization": MatchesListwise(
> +                            [
> +                                Equals(
> +                                    b"Basic "
> +                                    + base64.b64encode(
> +                                        b"admin-launchpad.test:admin-secret"
> +                                    )
> +                                )
> +                            ]
> +                        ),
> +                        b"Content-Type": MatchesListwise(
> +                            [
> +                                Equals(b"application/json"),
> +                            ]
> +                        ),
> +                    }
> +                ),
> +                "json": MatchesDict(
> +                    {
> +                        "username": StartsWith(job.build.build_cookie + "-"),
> +                    }
> +                ),
> +            }
> +        )
> +        self.assertThat(
> +            self.fetch_service_api.tokens.requests,
> +            MatchesListwise([request_matcher]),
> +        )
> +
> +    @defer.inlineCallbacks
>      def test_requestProxyToken_unconfigured(self):
>          self.pushConfig("builddmaster", builder_proxy_host=None)
>          branch = self.factory.makeBranch()


-- 
https://code.launchpad.net/~pelpsi/launchpad/+git/launchpad/+merge/461721
Your team Launchpad code reviewers is requested to review the proposed merge of ~pelpsi/launchpad:fetch-service-session-API into launchpad:master.



References