Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #539 http: add client_id and authentication #543

Merged
merged 26 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 167 additions & 122 deletions pykern/http.py

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions pykern/http_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

class Setup:

AUTH_TOKEN = "http_unit_auth_secret"

def __init__(self, api_classes, attr_classes=(), coros=()):
import os, time
from pykern.pkcollections import PKDict
Expand All @@ -29,21 +31,26 @@ def _http_config():
# any uri is fine
api_uri="/http_unit",
# just needs to be >= 16 word (required by http) chars; apps should generate this randomly
auth_secret="http_unit_auth_secret",
tcp_ip=pkconst.LOCALHOST_IP,
tcp_port=pkunit.unbound_localhost_tcp_port(),
)

def _server():
from pykern import pkdebug, http

def _api_classes():
class AuthAPI(http.AuthAPI):
PYKERN_HTTP_TOKEN = self.AUTH_TOKEN

return list(api_classes) + [AuthAPI]

if rv := os.fork():
return rv
try:
pkdebug.pkdlog("start server")
http.server_start(
attr_classes=attr_classes,
api_classes=api_classes,
api_classes=_api_classes(),
http_config=self.http_config.copy(),
coros=coros,
)
Expand All @@ -65,9 +72,12 @@ def destroy(self):

os.kill(self.server_pid, signal.SIGKILL)

def __enter__(self):
async def __aenter__(self):
from pykern import http

await self.client.connect(http.AuthArgs(token=self.AUTH_TOKEN))
return self.client

def __exit__(self, *args, **kwargs):
async def __aexit__(self, *args, **kwargs):
self.client.destroy()
return False
4 changes: 2 additions & 2 deletions pykern/pkcollections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""PKDict abstraction and utils

`PKDict` is similar to :class:`argparse.Namespace`, but is a dict that allows
Expand All @@ -7,6 +6,7 @@
:copyright: Copyright (c) 2015-2022 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""

# Limit pykern imports so avoid dependency issues for pkconfig
import copy
import collections.abc
Expand Down Expand Up @@ -63,7 +63,7 @@ def __setattr__(self, name, value):
raise PKDictNameError(
"{}: invalid key for PKDict matches existing attribute".format(name)
)
super(PKDict, self).__setitem__(name, value)
self[name] = value

def copy(self):
"""Override `dict.copy` to ensure the class of the return object is correct.
Expand Down
8 changes: 4 additions & 4 deletions pykern/pksubprocess.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""Wrapper for subprocess.

:copyright: Copyright (c) 2016 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from __future__ import absolute_import, division, print_function
from pykern.pkdebug import pkdc, pkdexc, pkdp

from pykern.pkdebug import pkdc, pkdexc, pkdp, pkdlog
import os
import signal
import six
Expand Down Expand Up @@ -43,6 +42,7 @@ def signal_handler(sig, frame):
if p:
p.send_signal(sig)
except Exception:
# Nothing we can do, still want to cascade
pass
finally:
ps = prev_signal[sig]
Expand Down Expand Up @@ -111,7 +111,7 @@ def wait_pid():
finally:
for sig in _SIGNALS:
signal.signal(sig, prev_signal[sig])
if not p is None:
if p is not None:
if msg:
msg("{}: terminating: {}", pid, cmd)
try:
Expand Down
40 changes: 33 additions & 7 deletions pykern/quest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,48 @@ def __init__(self, fmt, *args, **kwargs):


class Attr(PKDict):
# Class names bound to attribute keys
_KEY_MAP = PKDict()
#: shared Attrs do not have link to qcall
IS_SINGLETON = False

def __init__(self, qcall, **kwargs):
"""Initialize object from a qcall
"""Initialize object

Subclasses must define ATTR_KEY so it can be added to qcall.

If `IS_SINGLETON` is true then qcall must be None. This will
only be called outside of init_quest. Otherwise, qcall is bound to instance.

Args:
qcall (API): what qcall is being initialized
kwargs (dict): insert into dictionary

"""
super().__init__(qcall=qcall, **kwargs)
qcall.attr_set(self.ATTR_KEY, self)
if self.IS_SINGLETON:
assert qcall is None
super().__init__(**kwargs)
else:
super().__init__(qcall=qcall, **kwargs)

@classmethod
def init_quest(cls, qcall):
cls(qcall)
def init_quest(cls, qcall, **kwargs):
"""Initialize an instance of cls and put on qcall

If `IS_SINGLETON`, qcall is not put on self. `kwargs` must contain ATTR_KEY,
which is an instance of class.

Args:
qcall (API): quest
kwargs (**): values to passed to `start`
"""
if not cls.IS_SINGLETON:
self = cls(qcall, **kwargs)
elif (self := kwargs.get(cls.ATTR_KEY)) is None:
raise AssertionError(f"init_quest.kwargs does not contain {cls.ATTR_KEY}")
elif not isinstance(self, cls):
raise AssertionError(
f"init_quest.kwargs.{cls.ATTR_KEY}={self} not instance of {cls}"
)
qcall.attr_set(self.ATTR_KEY, self)


class Spec:
Expand Down
2 changes: 1 addition & 1 deletion test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
set -euo pipefail

_main() {
PYKERN_PKCLI_TEST_MAX_PROCS=4 pykern ci run
PYKERN_PKDEBUG_WANT_PID_TIME=1 PYKERN_PKCLI_TEST_MAX_PROCS=4 pykern ci run
}

_main "$@"
10 changes: 6 additions & 4 deletions tests/http_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
async def test_basic():
from pykern import http_unit

with http_unit.Setup(api_classes=(_class(),)) as c:
async with http_unit.Setup(api_classes=(_class(),)) as c:
from pykern.pkcollections import PKDict
from pykern import pkunit

e = PKDict(a=1)
pkunit.pkeq(e, await c.call_api("echo", e))
e = PKDict(ping="pong")
pkunit.pkeq(e.pkupdate(counter=1), await c.call_api("echo", e))
pkunit.pkeq(e.pkupdate(counter=2), await c.call_api("echo", e))


def _class():
Expand All @@ -25,6 +26,7 @@ def _class():
class _API(quest.API):

async def api_echo(self, api_args):
return api_args
self.session.pksetdefault(counter=0).counter += 1
return api_args.pkupdate(counter=self.session.counter)

return _API
54 changes: 36 additions & 18 deletions tests/pksubprocess_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
def test_check_call_with_signals():
from pykern import pksubprocess
from pykern import pkunit, pkcompat
from pykern import pkunit, pkcompat, pkdebug
import os
import signal
import subprocess
Expand Down Expand Up @@ -59,44 +59,62 @@ def signal_handler(sig, frame):
signals = []
signal.signal(signal.SIGTERM, signal_handler)
with open("kill.sh", "w") as f:
f.write("kill -TERM {}\nsleep 10".format(os.getpid()))
# Need to wait before sending signal, because subprocess needs to allow
# this process (parent) to run
f.write(
f"""
sleep .2
kill -TERM {os.getpid()}
sleep 5
echo FAIL
"""
)
cmd = ["sh", "kill.sh"]
with pytest.raises(RuntimeError):
exc = None
try:
pksubprocess.check_call_with_signals(cmd, output=o, msg=msg)
except RuntimeError as e:
exc = e
except BaseException as e:
pkunit.pkfail("unexpected exception={}", e)
o.seek(0)
actual = o.read()
assert "" == actual, 'Expecting empty output "{}"'.format(actual)
assert signal.SIGTERM in signals, '"SIGTERM" not in signals "{}"'.format(
signals
pkunit.pkeq("", actual)
pkunit.pkok(
signal.SIGTERM in signals, '"SIGTERM" not in signals "{}"', signals
)
assert (
"error exit" in messages[1]
), '"error exit" not in messages[1] "{}"'.format(messages[1])
pkunit.pkre("error exit", messages[1])
if exc is None:
pkunit.pkfail("exception was not raised")

with open("kill.out", "w+") as o:
messages = []
signals = []
signal.signal(signal.SIGTERM, signal_handler)
with open("kill.sh", "w") as f:
f.write(
"""
setsid bash -c "sleep 1; echo hello; setsid sleep 1313 & disown" &
f"""
setsid bash -c "sleep 1; echo FAIL; setsid sleep 1313 & disown" &
disown
sleep .3
kill -TERM {}
sleep .1
""".format(
os.getpid()
)
sleep .2
kill -TERM {os.getpid()}
sleep 5
echo FAIL
"""
)
cmd = ["bash", "kill.sh"]
with pytest.raises(RuntimeError):
exc = None
try:
pksubprocess.check_call_with_signals(
cmd,
output=o,
msg=msg,
recursive_kill=True,
)
except RuntimeError as e:
exc = e
except BaseException as e:
pkunit.pkfail("unexpected exception={}", e)
time.sleep(2)
o.seek(0)
actual = o.read()
Expand Down
Loading