Skip to content

Commit

Permalink
chore: ipython shell in a Docker container
Browse files Browse the repository at this point in the history
Initial support for running ipython shell as a docker compose service
that connects to other services running locally.
  • Loading branch information
ibukanov committed Jul 10, 2024
1 parent 435d166 commit 761d1a2
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 161 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,4 @@ nitro-shim/tools/eifbuild/third_party/**
nitro-image.eif
bat-go-repro.tar

# File with test keys
/payments-test-secretes.json
/build/
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ codeql: download-mod buildcmd
buildcmd:
cd main && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-w -s -X main.version=${GIT_VERSION} -X main.buildTime=${BUILD_TIME} -X main.commit=${GIT_COMMIT}" -o ${OUTPUT}/bat-go main.go

builddev:
test -d build || mkdir build
cd main && go build -o ../build/bat-go main.go

mock:
cd services && mockgen -source=./promotion/claim.go -destination=promotion/mockclaim.go -package=promotion
cd services && mockgen -source=./promotion/drain.go -destination=promotion/mockdrain.go -package=promotion
Expand Down Expand Up @@ -102,7 +106,7 @@ docker:
docker tag bat-go:$(GIT_VERSION)$(BUILD_TIME) bat-go:latest

docker-local:
docker build -t bat-go-local -f local-dev/local.dockerfile --target image .
docker build -t bat-go-local -f local-dev/local.dockerfile .

docker-reproducible:
$(eval TMP_CHECKOUT = $(shell mktemp -d 2>/dev/null || mktemp -d -t 'bat-go-tmp'))
Expand Down
63 changes: 55 additions & 8 deletions docker-compose.payments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ services:
image: redis:7.0
restart: always
#ports:
# - '6379:6379'
command: redis-server --save 20 1 --loglevel verbose --requirepass testpass --user redis
# - '6380:6380'
command: redis-server --save 20 1 --loglevel verbose --requirepass testpass --user redis --port 6380
#volumes:
# - redis-cache:/data

Expand All @@ -17,7 +17,7 @@ services:
localstack:
container_name: localstack
image: localstack/localstack
stop_grace_period: 1s
stop_grace_period: "1s"
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
Expand All @@ -31,13 +31,19 @@ services:

worker:
image: bat-go-local
command: /build/bat-go serve payments worker
depends_on:
- redis
- localstack
volumes:
- .:/workspace
command: >-
/workspace/local-dev/run-as-workspace-user.sh
/workspace/local-dev/run-until-rebuild.sh
/workspace/build/bat-go serve payments worker
restart: unless-stopped
environment:
- NITRO_ENCLAVE_MOCKING=1
- REDIS_ADDR=redis:6379
- REDIS_ADDR=redis:6380
- REDIS_USER=default
- REDIS_PASS=testpass
- DEBUG=1
Expand All @@ -46,19 +52,30 @@ services:

service:
image: bat-go-local
command: /build/bat-go serve nitro inside-enclave --egress-address none --log-address none --upstream-url http://0.0.0.0:8080
depends_on:
- redis
- localstack
volumes:
- ./payments-test-secretes.json:/etc/bat-test-secretes.json:ro
- .:/workspace
command: >-
/workspace/local-dev/run-as-workspace-user.sh
/workspace/local-dev/run-until-rebuild.sh
/workspace/build/bat-go serve nitro inside-enclave
--egress-address none --log-address none
--upstream-url http://0.0.0.0:8080
working_dir: /workspace
restart: unless-stopped
environment:
- NITRO_ENCLAVE_MOCKING=1
- BAT_PAYMENT_TEST_SECRETS=/workspace/local-dev/secrets/payments-test.json
- DEBUG=1
- ADDR=0.0.0.0:18080
- ADDR2=0.0.0.0:18443
- AWS_REGION="us-west-2"
- QLDB_LEDGER_ARN=arn:aws:qldb:us-west-2:239563960694:ledger/testing-igor-payment
- QLDB_LEDGER_NAME=testing-igor-payment
- QLDB_ROLE_ARN=arn:aws:iam::239563960694:role/settlements-dev-sso-qldb-20240628161845683100000001
- AWS_REGION=${AWS_REGION-}
- AWS_REGION=${AWS_REGION-us-west-2}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN-}
Expand All @@ -69,6 +86,36 @@ services:
- "127.0.0.1:18080:18080"
- "127.0.0.1:18443:18443"

shell:
image: bat-go-local
depends_on:
- redis
- localstack
volumes:
- .:/workspace
stop_grace_period: "1s"
# The shell uses localhost for redis by default, so use the redis container
# network for that to work.
network_mode: "service:redis"
command: >-
/workspace/local-dev/run-as-workspace-user.sh
ipython3 --profile-dir=ipython-profile
working_dir: /workspace/tools/payments
stdin_open: true
tty: true
environment:
- NITRO_ENCLAVE_MOCKING=1
- DEBUG=1
- REDIS_ADDR=redis:6379
- REDIS_USERNAME=default
- REDIS_PASSWORD=testpass
- AWS_REGION=${AWS_REGION-us-west-2}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY-}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN-}
- AWS_CONTAINER_CREDENTIALS_FULL_URI=${AWS_CONTAINER_CREDENTIALS_FULL_URI-}
- AWS_CONTAINER_AUTHORIZATION_TOKEN=${AWS_CONTAINER_AUTHORIZATION_TOKEN-}

volumes:
redis-cache:
driver: local
5 changes: 5 additions & 0 deletions local-dev/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Local files with development scretes.
/secrets/

# Home in the container
/home/
47 changes: 47 additions & 0 deletions local-dev/ensure-secretes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/sh

set -eu

self="$(realpath "$0")"
secrets="${self%/*}/secrets"

test -d "$secrets" || {
echo "Creating $secrets directory" >&2
mkdir -m 0700 "$secrets"
}

f="$secrets/payments-test.json"
test -s "$f" || {
echo "The file with configuration keys $f does not exist or empty, please obtain it"
exit 1
}

# We need to generate ED25519 key in PEM format and its public key in OpenSSH
# format. Unfortunately ssh-keygen released only 2024 supports. So use a
# workaround.

pem="$secrets/payment-test-operator.pem"
test -s "$pem" || {
echo "ED25519 private key file $pem does not exist, generating it" >&2
x="$(command -v openssl 2>/dev/null || :)"
test "$x" || {
echo "openssl tool does not exist, please install it. On Debian-based system use:" >&2
echo " apt install openssl" >&2
exit 1
}
rm -f "$pem.tmp"
openssl genpkey -algorithm ed25519 > "$pem.tmp"
mv "$pem.tmp" "$pem"
}
pub="${pem%.pem}.pub"
test -s "$pub" || {
echo "ED25519 public key file $pub does not exist, producing it from" >&2
x="$(command -v sshpk-conv 2>/dev/null || :)"
test "$x" || {
echo "sshpk-conv utility does not exist, please install it. On Debian-based system use:" >&2
echo " apt install node-sshpk" >&2
exit 1
}
sshpk-conv -T pem -t ssh -f "$pem" -o "$pub"
}

46 changes: 9 additions & 37 deletions local-dev/local.dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
FROM debian:bookworm AS base
FROM debian:bookworm

ARG DEBIAN_FRONTEND=noninteractive
ARG GOLANG_VERSION=1.22.4

RUN apt-get update \
&& apt-get install -y -qq \
tmux curl man less \
python3 git make
tmux curl man less openssh-client openssl util-linux \
python3 ipython3 python-is-python3 \
socat lsof wget diffutils \
git make \
redis-tools \
node-sshpk

# Install Go
RUN set -x && curl -L -o /var/tmp/go.tgz \
Expand All @@ -16,37 +20,5 @@ RUN set -x && curl -L -o /var/tmp/go.tgz \
&& find /usr/local/go/bin -type f -perm /001 \
-exec ln -s -t /usr/local/bin '{}' +

RUN useradd -m user

RUN mkdir /build && chown user:user /build

USER user
WORKDIR /home/user

#CMD [ "sleep", "infinity" ]

# A helper stage to hold Go sources with go.mod and related infrequently changed
# files moved to separated directory so they can be copied later before the *.go
# files to allow to cache downloaded Go modules in a Docker layer independent
# from more frequently changed sources.
FROM base as sources

COPY --chown=user:user . /build/repo
RUN mkdir /build/mod-files && cd /build/repo && rm -rf .git \
&& find . -name go.\* | xargs tar cf - | tar -C /build/mod-files -xf - \
&& find . -name go.\* -delete

FROM base as image

RUN mkdir -p .cache

COPY --link --from=sources --chown=user:user /build/mod-files/ /build/src/

RUN cd /build/src/main && go mod download -x

COPY --link --from=sources --chown=user:user /build/repo/ /build/src/

RUN cd /build/src/main \
&& CGO_ENABLED=0 GOOS=linux go build \
-o /build/bat-go main.go

# Use arbitrary id, not 1000, to always test the code to sync the user id.
RUN useradd -m -u 12345 user
37 changes: 37 additions & 0 deletions local-dev/run-as-workspace-user.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh

# Helper to sync the container user with the id of the owner of workspace and
# then run the command as that user.

set -eu

workspace_uid_gid="$(stat -c %u:%g /workspace)"
uid="${workspace_uid_gid%:*}"
gid="${workspace_uid_gid#*:}"

h="/workspace/local-dev/home"

if ! test -h /home/user; then
# The user in the container not yet adjusted
groupmod -g "$gid" user
usermod -u "$uid" user

# Updating files inside /workspace may race with other containers. So to
# copy skeleton first copy it to a temporeary location and then move
# atomically.
if ! test -d "$h"; then
echo "Creating $h" >&2
chown -R user:user /home/user
tmp="$(mktemp -u -p "${h%/*}")"
cp -a /home/user "$tmp" || { rm -rf "$tmp"; exit 1; }
mv "$tmp" "$h" || { rm -rf "$tmp"; exit 1; }
fi
rm -rf /home/user
ln -s "$h" /home/user
fi

# We want to keep the current environmnet for the subprocess so we do not use
# --reset-env with setprov. Rather we just fixup few variables using env.
exec setpriv --init-groups --regid "$gid" --reuid "$uid" --no-new-privs \
env HOME="$h" SHELL=/usr/bin/bash USER=user LOGNAME=user \
PATH=/usr/local/bin:/usr/bin "$@"
36 changes: 36 additions & 0 deletions local-dev/run-until-rebuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/sh

# Helper to run a command with its arguments until the timestamp of its
# executable changes.

set -eu

target="$1"

test -x "$1" || {
echo "The argument $1 is not an executable" >&2
exit 1
}

monitor() {
local modification_time t
modification_time="$(stat -c "%Y" "$target")"
while :; do
sleep 1
t="$(stat -c "%Y" "$target")"
if test "$t" -ne "$modification_time"; then
echo "Newer $target is detected, restarting" >&2
kill "$$"
sleep 0.5
kill -9 "$$"
fi
done
}

monitor &

bar="======================================================================"
t="$(date '+%Y-%m-%d %H:%M:%S')"
printf '%s\n[%s] Running\n[%s] %s\n%s\n' "$bar" "$t" "$t" "$*" "$bar" >&2

exec "$@"
13 changes: 13 additions & 0 deletions services/payments/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"os"
"strings"

"github.com/brave-intl/bat-go/libs/httpsignature"
"github.com/brave-intl/bat-go/libs/nitro"
paymentLib "github.com/brave-intl/bat-go/libs/payments"
"golang.org/x/crypto/ssh"
)
Expand All @@ -24,6 +26,17 @@ func init() {
validAuthorizers[hex.EncodeToString(pub)] = pub
}
fmt.Println(validAuthorizers)
if nitro.EnclaveMocking() {
keyBytes, err := os.ReadFile("/workspace/local-dev/secrets/payment-test-operator.pub")
if err != nil {
panic(err)
}
pub, err := DecodePublicKey(string(keyBytes))
if err != nil {
panic(err)
}
validAuthorizers[hex.EncodeToString(pub)] = pub
}
}

// DecodePublicKey decodes the public key which can either be a raw hex encoded ed25519 public key or an ssh-ed25519 public key
Expand Down
12 changes: 8 additions & 4 deletions services/payments/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,16 +565,20 @@ func (u *Unsealing) decryptSecrets(ctx context.Context) error {
}

func (u *Unsealing) readTestSecretes() error {
testPath := "/etc/bat-test-secretes.json"
f, err := os.Open(testPath)
envName := "BAT_PAYMENT_TEST_SECRETS"
secretsPath := os.Getenv(envName)
if secretsPath == "" {
return fmt.Errorf("The environment variable %s is not set", envName)
}
f, err := os.Open(secretsPath)
if err != nil {
return err
return fmt.Errorf("Failed to open the test secrets from %s - %w", envName, err)
}

output := map[string]string{}
if err := json.NewDecoder(f).Decode(&output); err != nil {
return fmt.Errorf(
"failed to json decode the test secretes %s: %w", testPath, err)
"failed to json decode the test secretes %s: %w", secretsPath, err)
}
u.secrets = output
return nil
Expand Down
Loading

0 comments on commit 761d1a2

Please sign in to comment.