diff --git a/.dockerignore b/.dockerignore index 0c11a11..1aff11c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ -*.yml -*.csv script/__pycache__** +**/*.md +.gitignore diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml new file mode 100644 index 0000000..b3ab9be --- /dev/null +++ b/.github/workflows/docker-build-and-push.yml @@ -0,0 +1,132 @@ +name: Create and publish a Docker image + +on: + push: + branches: ["**"] # run for any commit or tag push on any branch + tags-ignore: ["v**"] # reserve v* tags for releases + +env: + REGISTRY_IMAGE: ghcr.io/novatecconsulting/grafana-ldap-sync-script + +jobs: + build: + name: Build and publish docker image for grafana-ldap-sync-script on ${{ matrix.platform }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + include: + - platform: linux/arm64 + os: [ubuntu-latest] + - platform: linux/amd64 + os: [ubuntu-latest] + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Prepare for grafana-ldap-sync-script on ${{ matrix.platform }} + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Extract metadata (tags, labels) for Docker from grafana-ldap-sync-script + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push by digest for grafana-ldap-sync-script ${{ matrix.platform }} + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }} + + - name: Export digest for grafana-ldap-sync-script on ${{ matrix.platform }} + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-grafana-ldap-sync-script-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: + - build + strategy: + fail-fast: false + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-grafana-ldap-sync-script* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/Dockerfile b/Dockerfile index 06cdcdb..4afe1ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,33 @@ -FROM python:3.9-slim +FROM python:3.13-slim AS installer +RUN apt-get update +RUN apt-get install -y --no-install-recommends build-essential gcc -COPY requirements.txt /requirements.txt +RUN python -m venv /opt/venv +# Make sure we use the virtualenv: +ENV PATH="/opt/venv/bin:$PATH" -RUN pip install -r /requirements.txt && mkdir /app +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY script ./script +COPY setup.py . +RUN pip install . + +FROM python:3.13-slim AS runtime +COPY --from=installer /opt/venv /opt/venv + +WORKDIR /data +COPY config.yml . +COPY example.csv /data/bind.csv WORKDIR /app +# Make sure we use the virtualenv: +ENV PATH="/opt/venv/bin:$PATH" + +COPY run.py . -COPY LICENSE run.py /app/ -COPY script /app/script +VOLUME [ "/data" ] -ENTRYPOINT [ "python3", "./run.py" ] +CMD [ "python", "run.py", "--config=/data/config.yml", "--bind=/data/bind.csv" ] diff --git a/config.yml b/config.yml index 0e1ef0d..db19d99 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,5 @@ config: grafana: - # URL of the target grafana-server. url: localhost:3000 # Protocol to use when connecting to grafana (http, https) @@ -9,6 +8,8 @@ config: user: admin # Password of account with admin rights on target grafana-server. password: admin + # Grafana Organization ID that should be used to insert the teams. + # org_id: 1 ldap: # Set to True if NTLM should be used for LDAP. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4b901ca --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + grafana: + image: grafana/grafana:11.2.2 + restart: unless-stopped + volumes: + - grafana-storage:/var/lib/grafana + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] + interval: 1m + timeout: 1s + retries: 3 + ports: + - 3000:3000 + attach: false + + grafana-ldap-sync-script: + build: + dockerfile: Dockerfile + volumes: + - ./example.csv:/data/bind.csv:ro + # you need to change grafana.config.url to 'grafana:3000' from 'localhost:3000' for this to work! + - ./config.yml:/data/config.yml:ro + depends_on: + grafana: + condition: service_healthy + + +volumes: + grafana-storage: diff --git a/requirements.txt b/requirements.txt index 1c4204b..3c15be9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -requests~=2.24.0 -grafana_client~=2.0.2 -ldap3~=2.6 -PyYAML~=5.3.1 -pyasn1>=0.4.6 \ No newline at end of file +requests~=2.32.3 +grafana_client~=4.2.0 +ldap3~=2.9.1 +PyYAML~=6.0.2 +pyasn1>=0.4.6 diff --git a/run.py b/run.py index c5928b2..ded47cc 100644 --- a/run.py +++ b/run.py @@ -1,7 +1,9 @@ -from script.core import startUserSync import argparse import logging +from script.core import startUserSync + + class DispatchingFormatter: def __init__(self, formatters, default_formatter): self._formatters = formatters @@ -23,7 +25,7 @@ def setup_logger(): else: log_format_mut = log_format - + logger = logging.getLogger() while logger.handlers: logger.handlers.pop() diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/script/config.py b/script/config.py index 1f54b75..2af28df 100644 --- a/script/config.py +++ b/script/config.py @@ -1,4 +1,5 @@ import logging + import yaml logging.basicConfig(level=logging.INFO) @@ -15,6 +16,7 @@ def __init__(self, config_path): GRAFANA_AUTH = "" GRAFANA_URL = "" GRAFANA_PROTOCOL = "http" + GRAFANA_ORG_ID = None LDAP_SERVER_URL = "" LDAP_PORT = "" @@ -49,6 +51,7 @@ def load_config(self, config_path): self.GRAFANA_URL = config["grafana"]["url"] if config["grafana"]["protocol"]: self.GRAFANA_PROTOCOL = config["grafana"]["protocol"] + self.GRAFANA_ORG_ID = config["grafana"]["org_id"] if "org_id" in config["grafana"] else None self.LDAP_SERVER_URL = config["ldap"]["url"] self.LDAP_PORT = config["ldap"]["port"] diff --git a/script/grafana.py b/script/grafana.py index d926a6a..0f98e1a 100644 --- a/script/grafana.py +++ b/script/grafana.py @@ -1,8 +1,10 @@ -from grafana_client.client import GrafanaClientError, GrafanaBadInputError +import logging + from grafana_client import GrafanaApi +from grafana_client.client import GrafanaBadInputError, GrafanaClientError + from .config import * from .helpers import * -import logging grafana_api = "" configuration = "" @@ -16,7 +18,8 @@ def setup_grafana(config_dict): grafana_api = GrafanaApi( auth=configuration.GRAFANA_AUTH, host=configuration.GRAFANA_URL, - protocol=configuration.GRAFANA_PROTOCOL + protocol=configuration.GRAFANA_PROTOCOL, + organization_id=configuration.GRAFANA_ORG_ID ) diff --git a/setup.py b/setup.py index ffd93cd..59512f3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( - name='grafana-ldap-sync-script', + name='script', version='1.1.0', description='Script for syncing LDAP Users & Groups with Grafana Users & Teams', packages=find_packages(exclude=('tests', 'docs')),