Skip to content

Commit

Permalink
Basic repo feeding, bearer token auth and locking (useless)
Browse files Browse the repository at this point in the history
Also a Dockerfile to build an env capable of running the whole thing,
including borgbackup.
  • Loading branch information
Jinna Kiisuo committed Aug 27, 2023
1 parent 70f676f commit 9ba0e20
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 6 deletions.
55 changes: 55 additions & 0 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Docker image

on: push

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Get the release channel
id: get_channel
shell: bash
run: |
if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
echo ::set-output name=channel::"latest"
echo ::set-output name=version::main_${GITHUB_SHA::6}
elif [[ "$GITHUB_REF" == "refs/heads/"* ]]; then
echo ::set-output name=version::${GITHUB_REF/refs\/heads\//}_${GITHUB_SHA::6}
elif [[ "$GITHUB_REF" == "refs/tags/"* ]]; then
echo ::set-output name=channel::${GITHUB_REF/refs\/tags\//}
fi
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ steps.get_channel.outputs.channel }}
type=raw,value=${{ steps.get_channel.outputs.version }}
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.11-buster AS poetry_builder
ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN curl -sSL https://install.python-poetry.org | python -


FROM poetry_builder as builder
RUN mkdir /build
WORKDIR /build
RUN apt update && apt install -y build-essential libfuse-dev libacl1-dev
RUN pip wheel borgbackup
COPY borg_lockservice ./borg_lockservice
COPY README.md ./README.md
COPY poetry.lock pyproject.toml ./
RUN poetry build -f wheel


FROM python:3.11-slim-buster
WORKDIR /srv
COPY --from=builder /build/*.whl /build/dist/*.whl ./
RUN pip install *.whl

ENTRYPOINT ["borg_lockservice"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
This is highly WIP. The design intent is to have a tiny service running on the node with borgbackup repos to handle locking them. This allows dependent upstream services, such as offsite sync jobs, to perform their work without having write access to the repositories.

### Debian build dependencies
Since some packges may not have wheels available, the following were observed as needed on Debian 12 to build:
- `libacl1-dev`
- `libfuse-dev`
53 changes: 49 additions & 4 deletions borg_lockservice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer

import subprocess
import pathlib

# Get all directories under the given paths
def get_available_repos(directory: str) -> list[pathlib.Path]:
path = pathlib.Path(directory)
return [f for f in path.iterdir() if f.is_dir()]


FLAGS = flags.FLAGS
app = FastAPI()
Expand All @@ -21,6 +29,12 @@
"Bearer token required to access the API.",
)

flags.DEFINE_string(
"repodir",
os.getenv(f"{PREFIX}_REPODIR", None),
"Directory containing repos.",
)

flags.DEFINE_string(
"host",
os.getenv(f"{PREFIX}_HOST", "0.0.0.0"),
Expand All @@ -38,10 +52,11 @@
os.getenv(f"{PREFIX}_DEV", False),
"Enable development mode. Defaults to False, should not be enabled in production.",
)

flags.mark_flag_as_required('repodir')

FLAGS(sys.argv)
BEARER_TOKEN = FLAGS.token
REPOS: list[pathlib.Path] = get_available_repos(FLAGS.repodir)


@app.get("/")
Expand All @@ -53,10 +68,18 @@ async def root():

@app.get("/lock/{repo}")
async def lock(
repo: str, token: Annotated[str, Depends(auth)], timeout_minutes: int = 60
repo: str,
token: Annotated[str, Depends(auth)],
timeout_seconds: int = 3600,
duration_minutes: int = 45,
):
if token.credentials == BEARER_TOKEN:
return {"message": f"Totally locked {repo} for {timeout_minutes}m"}
repo_path: pathlib.Path = get_repo_path(repo)
if repo_path:
acquire_lock(repo_path, timeout_seconds, duration_minutes)
return {"message": f"Locked {repo} for a max of {duration_minutes}m"}
else:
raise HTTPException(status_code=404)
else:
raise HTTPException(status_code=403)

Expand All @@ -73,7 +96,29 @@ async def status(repo: str):

@app.get("/list")
async def list_locks():
return {"message": "Not yet implemented"}
return {"repos": REPOS}


def get_repo_path(target: str) -> pathlib.Path:
for repo in REPOS:
if repo.name == target:
return repo
return None

def acquire_lock(repo: pathlib.Path, timeout_seconds: int, duration_minutes: int):
try:
subprocess.run(
[
"borg",
"with-lock",
f"--lock-wait={timeout_seconds}",
repo,
"sleep",
f"{duration_minutes}m",
]
)
except subprocess.CalledProcessError:
raise HTTPException(status_code=423) # 423: Locked


def run():
Expand Down
120 changes: 118 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ python = "^3.11"
uvicorn = {extras = ["standard"], version = "^0.23.2"}
absl-py = "^1.4.0"
fastapi = "^0.101.1"
borgbackup = "^1.2.4"


[tool.poetry.group.dev.dependencies]
black = "^23.7.0"
flake8 = "^6.1.0"
mypy = "^1.5.1"
types-pyyaml = "^6.0.12.11"
types-ujson = "^5.8.0.1"

[build-system]
requires = ["poetry-core"]
Expand Down

0 comments on commit 9ba0e20

Please sign in to comment.