diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..138f099 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.DS_Store +.idea +*.log +tmp/ + +.direnv/ + +dist-newstyle/ +result +result-* +out/ diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..199f09d --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,116 @@ +name: Docker + +on: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - "v*" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and cache Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + cache-from: type=gha + cache-to: type=gha,mode=max + push: false + + push: + needs: build + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,format=long + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.gitignore b/.gitignore index a240499..138f099 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tmp/ dist-newstyle/ result result-* +out/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0b90f70 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# We can't use the official haskell image, because it's based on Debian, +# which uses glibc, and we need musl to build a static binary. +FROM benz0li/ghc-musl:9.6 AS builder + +# Install dependencies first, to cache them +WORKDIR /app +COPY tailscale-manager.cabal . +RUN cabal update && cabal build -j --only-dependencies + +# Copy source code +COPY . . + +# Build static binary, code from https://hasufell.github.io/posts/2024-04-21-static-linking.html +RUN cabal build -j --enable-executable-static exe:tailscale-manager +RUN mkdir out/ && cp $(cabal -v0 list-bin exe:tailscale-manager) out/ + +# Copy the binary to a new image, to keep the final image lightweight +FROM alpine:3.20 AS runtime + +COPY --from=builder /app/out/tailscale-manager /bin/ +CMD ["tailscale-manager"] diff --git a/README.md b/README.md index 31cf1fd..19fbeed 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Tailscale routes manager **tailscale-manager** dynamically manages Tailscale subnet route advertisements -based on user-configurable discovery sources. It runs alongside tailscaled on +based on user-configurable discovery sources. It runs alongside tailscaled on the node(s) where you want to advertise routes. ## Supported discovery methods | config keyword | example | description | -|:------------------------|:------------------------------|:---------------------------| +| :---------------------- | :---------------------------- | :------------------------- | | `routes` | `["192.168.0.0/24"]` | Static routes | | `hostRoutes` | `["private-app.example.com"]` | DNS hostname lookup | | `awsManagedPrefixLists` | `["pl-02761f4a40454a3c9"]` | [AWS Managed Prefix Lists] | @@ -16,7 +16,7 @@ the node(s) where you want to advertise routes. `hostRoutes` can be used to emulate [Tailscale App Connectors] by advertising a set of individual IP address routes that are kept in sync with DNS lookups of a -set of hostnames. This is most useful when using [Headscale], which doesn't +set of hostnames. This is most useful when using [Headscale], which doesn't normally support App Connectors. [Tailscale App Connectors]: https://tailscale.com/kb/1281/app-connectors @@ -35,11 +35,11 @@ normally support App Connectors. Here is a sample config file, in JSON format: -``` json +```json { "routes": [ "172.16.0.0/22", - "192.168.0.0/24", + "192.168.0.0/24" ], "hostRoutes": [ "github.com", @@ -104,11 +104,39 @@ Available options: -h,--help Show this help text ``` +## Docker image + +A Docker image is built and pushed to GitHub Container Registry on each version, commit, and pull request. The image is built based on Alpine images and contains a statically-linked `tailscale-manager` binary. + +You can use it to build your own custom Tailscale Docker images using the following Dockerfile and `entrypoint.sh` script. + +```dockerfile +# Dockerfile +FROM ghcr.io/singlestore-labs/tailscale-manager AS tailscale-manager +FROM tailscale/tailscale AS tailscale + +COPY --from=tailscale-manager /bin/tailscale-manager /bin/tailscale-manager + +COPY config.json + +COPY entrypoint.sh /usr/local/bin/entrypoint +CMD ["entrypoint"] +``` + +```sh +# entrypoint.sh +#!/bin/sh +tailscale-manager --interval 300 & +containerboot +``` + +This will allow you to use Tailscale as a Docker container while having `tailscale-manager` running in the background, periodically updating routes. + ## NixOS module -If you use NixOS, this repository provides a flake with a NixOS module to install and run tailscale-manager as a systemd service. You can incorporate it into your flake.nix like so: +If you use NixOS, this repository provides a flake with a NixOS module to install and run tailscale-manager as a systemd service. You can incorporate it into your flake.nix like so: -``` nix +```nix { description = "my nixos config";