commit f79b20e8a4f6399d680957b82de3181f60ce5200 Author: Markus Pesch Date: Sun Aug 18 20:49:30 2024 +0200 Initial Commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..48bdcd7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +cmd/autharr/autharr +cmd/healarr/healarr \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..394a9d4 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,727 @@ +--- +kind: pipeline +type: kubernetes +name: linter + +clone: + disable: true + +platform: + os: linux + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: markdown lint + commands: + - markdownlint *.md + image: git.cryptic.systems/volker.raschek/markdownlint:0.41.0 + resources: + limits: + cpu: 150 + memory: 150M + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + event: + exclude: + - tag + +--- +kind: pipeline +type: docker +name: unit-test-amd64 + +clone: + disable: true + +platform: + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: unit-test + commands: + - go test -v ./... + image: docker.io/library/golang:1.23.0 + +trigger: + event: + exclude: + - tag + +--- +kind: pipeline +type: docker +name: unit-test-arm64 + +clone: + disable: true + +platform: + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: unit-test + commands: + - go test -v ./... + image: docker.io/library/golang:1.23.0 + +trigger: + event: + include: + - pull_request + - push + exclude: + - tag + +--- +kind: pipeline +type: docker +name: dry-run-amd64 + +clone: + disable: true + +depends_on: +- linter +- unit-test-amd64 + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: false + dockerfile: Dockerfile + dry_run: true + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + tags: latest-amd64 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + exclude: + - master + event: + - pull_request + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: docker +name: dry-run-arm64-v8 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm64 + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: false + dockerfile: Dockerfile + dry_run: true + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + tags: latest-arm64-v8 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + exclude: + - master + event: + - pull_request + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: docker +name: latest-amd64 + +clone: + disable: true + +depends_on: +- linter +- unit-test-amd64 + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: false + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + tags: latest-amd64 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: docker +name: latest-arm64-v8 + +clone: + disable: true + +depends_on: +- linter +- unit-test-arm64 + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: false + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + tags: latest-arm64-v8 + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: kubernetes +name: latest-manifest + +clone: + disable: true + +depends_on: +- latest-amd64 +- latest-arm64-v8 + +# docker.io/plugins/manifest only for amd64 architectures available +node_selector: + kubernetes.io/os: linux + kubernetes.io/arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build-manifest + image: docker.io/plugins/manifest:1.4.0 + settings: + auto_tag: false + ignore_missing: true + spec: manifest.tmpl + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: kubernetes +name: latest-sync + +clone: + disable: true + +depends_on: +- latest-manifest + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: latest-sync + commands: + - skopeo sync --all --src=docker --src-creds=$SRC_CRED_USERNAME:$SRC_CRED_PASSWORD --dest=docker --dest-creds=$DEST_CRED_USERNAME:$DEST_CRED_PASSWORD git.cryptic.systems/volker.raschek/tarr docker.io/volkerraschek + environment: + SRC_CRED_USERNAME: + from_secret: git_cryptic_systems_container_registry_user + SRC_CRED_PASSWORD: + from_secret: git_cryptic_systems_container_registry_password + DEST_CRED_USERNAME: + from_secret: container_image_registry_user + DEST_CRED_PASSWORD: + from_secret: container_image_registry_password + image: quay.io/skopeo/stable:v1.16.0 + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + branch: + - master + event: + - cron + - push + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: docker +name: tagged-amd64 + +clone: + disable: true + +platform: + os: linux + arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: true + auto_tag_suffix: amd64 + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + build_args: + - VERSION=${DRONE_TAG} + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: docker +name: tagged-arm64-v8 + +clone: + disable: true + +platform: + os: linux + arch: arm64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build + image: docker.io/plugins/docker:20.18.3 + settings: + auto_tag: true + auto_tag_suffix: arm64-v8 + dockerfile: Dockerfile + force_tag: true + no_cache: true + purge: true + mirror: + from_secret: docker_io_mirror + registry: git.cryptic.systems + repo: git.cryptic.systems/volker.raschek/tarr + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + build_args: + - VERSION=${DRONE_TAG} + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: kubernetes +name: tagged-manifest + +clone: + disable: true + +depends_on: +- tagged-amd64 +- tagged-arm64-v8 + +# docker.io/plugins/manifest only for amd64 architectures available +node_selector: + kubernetes.io/os: linux + kubernetes.io/arch: amd64 + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: build-manifest + image: docker.io/plugins/manifest:1.4.0 + settings: + auto_tag: true + ignore_missing: true + spec: manifest.tmpl + username: + from_secret: git_cryptic_systems_container_registry_user + password: + from_secret: git_cryptic_systems_container_registry_password + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/tarr + +--- +kind: pipeline +type: kubernetes +name: tagged-sync + +clone: + disable: true + +depends_on: +- tagged-manifest + +steps: +- name: clone + image: git.cryptic.systems/volker.raschek/git:1.3.1 + +- name: tagged-sync + commands: + - skopeo sync --all --src=docker --src-creds=$SRC_CRED_USERNAME:$SRC_CRED_PASSWORD --dest=docker --dest-creds=$DEST_CRED_USERNAME:$DEST_CRED_PASSWORD git.cryptic.systems/volker.raschek/tarr docker.io/volkerraschek + environment: + SRC_CRED_USERNAME: + from_secret: git_cryptic_systems_container_registry_user + SRC_CRED_PASSWORD: + from_secret: git_cryptic_systems_container_registry_password + DEST_CRED_USERNAME: + from_secret: container_image_registry_user + DEST_CRED_PASSWORD: + from_secret: container_image_registry_password + image: quay.io/skopeo/stable:v1.16.0 + +- name: email-notification + environment: + SMTP_FROM_ADDRESS: + from_secret: smtp_from_address + SMTP_FROM_NAME: + from_secret: smtp_from_name + SMTP_HOST: + from_secret: smtp_host + SMTP_USERNAME: + from_secret: smtp_username + SMTP_PASSWORD: + from_secret: smtp_password + image: git.cryptic.systems/volker.raschek/drone-email:0.1.5 + resources: + limits: + cpu: 150 + memory: 150M + when: + status: + - changed + - failure + +trigger: + event: + - tag + repo: + - volker.raschek/tarr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48bdcd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cmd/autharr/autharr +cmd/healarr/healarr \ No newline at end of file diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..3bc2098 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,143 @@ +# markdownlint YAML configuration +# https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "atx" + +# MD004/ul-style - Unordered list style +MD004: + style: "dash" + +# MD007/ul-indent - Unordered list indentation +MD007: + # Spaces for indent + indent: 2 + # Whether to indent the first level of the list + start_indented: false + +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs - Hard tabs +MD010: + # Include code blocks + code_blocks: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 120 + # Number of characters for headings + heading_line_length: 120 + # Number of characters for code blocks + code_block_line_length: 120 + # Include code blocks + code_blocks: false + # Include tables + tables: false + # Include headings + headings: true + # Include headings + headers: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + # Only check sibling headings + allow_different_nesting: true + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD029/ol-prefix - Ordered list item prefix +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "---" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: false + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "fenced" + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence syle + style: "backtick" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd34bad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM docker.io/library/golang:1.23.0-alpine3.19 AS build + +RUN apk add git make + +WORKDIR /workspace +ADD ./ /workspace + +RUN make -C cmd/autharr install \ + DESTDIR=/cache \ + PREFIX=/usr \ + VERSION=${VERSION} + +RUN make -C cmd/healarr install \ + DESTDIR=/cache \ + PREFIX=/usr \ + VERSION=${VERSION} + +FROM docker.io/library/alpine:3.20 + +COPY --from=build /cache / \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b275dd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2024 Markus Pesch + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d4a515 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# tarr + +[![Build Status](https://drone.cryptic.systems/api/badges/volker.raschek/tarr/status.svg)](https://drone.cryptic.systems/volker.raschek/tarr) +[![Docker Pulls](https://img.shields.io/docker/pulls/volkerraschek/tarr)](https://hub.docker.com/r/volkerraschek/tarr) + +The tarr project contains small binaries / tools for interacting with *arr applications. The tools are helpful in a +kubernetes environment to retrofit missing functions of the \*arr applications. + +> [!NOTE] +> Instead of compiling the tarr applications by yourself, use the tarr container image instead. More described [below](#container-image). + +## autharr + +The binary `autharr` is a small program to extract from a `config.xml` or `config.yaml` the API token. The token is +written to the standard output. Alternatively, it can also be written to a file. + +With regard to [exportarr](https://github.com/onedr0p/exportarr), it can be helpful in the Kubernetes environment to +extract the token and use it for other API queries. For example for healthchecks. It therefore solve the following +[problem](https://github.com/onedr0p/exportarr/issues/294). + +```bash +$ autharr /etc/bazarr/config.yaml +do7IuHiewooFaiyu +$ autharr /etc/lidarr/config.xml +aeteipei4Meing5i +``` + +Alternatively, the `--watch` flag can be set. This monitors the config file and writes the API token to the defined +output in the event of changes. + +```bash +$ autharr --watch /etc/bazarr/config.yaml +baGohkie9EL5Tahr +oov1liQuaiki1lar +vaeGa9Cheeheev2I +``` + +Pipe the output direct into a file. Exit the program by Ctrl+C. + +```bash +$ autharr --watch /etc/bazarr/config.yaml /tmp/bazarr/token +^C +$ +``` + +## healarr + +The binary `healarr` is a small program to check if the *arr application is healthy. Some \*arr applications does not +have implemented a dedicated REST endpoint for healthchecks or like the liveness or readiness probe. Instead will be +called the API for a status, which returns 200 if the \*arr instance is healthy. + +`healarr` uses the internal packages from `autharr` to extract the API token from a config file. Alternatively can +directly passed the API token as flag. + +```bash +$ if healarr bazarr https://bazarr.example.com --config /etc/bazarr/config.xml; then +> echo "Healthy" +> else +> echo "Unhealthy" +> fi +Healthy +``` + +## container-image + +The container image `docker.io/volkerraschek/tarr` contains all tarr applications. The command below is an example to +start `autharr` of the container image `volkerraschek/tarr` via docker. `autharr` is watching for changes of the API +token. Any change will be written to the standard output. + +> [!NOTE] +> Adapt the volume mount, if you want to write the token to file on the host system. + +```bash +$ docker run \ + --rm \ + --volume /etc/bazarr:/etc/bazarr:ro \ + docker.io/volkerraschek/tarr:latest \ + autharr --watch /etc/bazarr/config.yaml +``` diff --git a/cmd/autharr/LICENSE b/cmd/autharr/LICENSE new file mode 100644 index 0000000..b275dd7 --- /dev/null +++ b/cmd/autharr/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2024 Markus Pesch + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/cmd/autharr/Makefile b/cmd/autharr/Makefile new file mode 100644 index 0000000..ec22b08 --- /dev/null +++ b/cmd/autharr/Makefile @@ -0,0 +1,37 @@ +VERSION?=$(shell git describe --abbrev=0)+hash.$(shell git rev-parse --short HEAD) + +# CONTAINER_RUNTIME +# The CONTAINER_RUNTIME variable will be used to specified the path to a +# container runtime. This is needed to start and run a container image. +CONTAINER_RUNTIME?=$(shell which podman) + +# BIN +# ============================================================================== +autharr: + CGO_ENABLED=0 \ + go build -ldflags "-X 'main.version=${VERSION}'" -o ${@} main.go + +# CLEAN +# ============================================================================== +PHONY+=clean +clean: + rm --force --recursive autharr + +# INSTALL +# ============================================================================== +PHONY+=install +install: autharr + # install --directory ${DESTDIR}/etc/bash_completion.d + # ./autharr completion bash > ${DESTDIR}/etc/bash_completion.d/autharr + + install --directory ${DESTDIR}${PREFIX}/bin + install --mode 0755 autharr ${DESTDIR}${PREFIX}/bin/autharr + + install --directory ${DESTDIR}${PREFIX}/share/licenses/autharr + install --mode 0644 LICENSE ${DESTDIR}${PREFIX}/share/licenses/autharr/LICENSE + +# PHONY +# ============================================================================== +# Declare the contents of the PHONY variable as phony. We keep that information +# in a variable so we can use it in if_changed. +.PHONY: ${PHONY} \ No newline at end of file diff --git a/cmd/autharr/main.go b/cmd/autharr/main.go new file mode 100644 index 0000000..00eef28 --- /dev/null +++ b/cmd/autharr/main.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "git.cryptic.systems/volker.raschek/tarr/pkg/config" + "git.cryptic.systems/volker.raschek/tarr/pkg/domain" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var version string + +func main() { + rootCmd := &cobra.Command{ + Args: cobra.RangeArgs(1, 2), + Long: `autharr reads the XML or YAML configuration file and prints the API token on stdout`, + Example: `autharr /etc/bazarr/config.yaml +autharr /etc/lidarr/config.xml`, + RunE: runE, + Version: version, + Use: "autharr", + } + rootCmd.Flags().Bool("watch", false, "Listens for changes to the configuration and writes the token continuously to the output") + + rootCmd.Execute() +} + +func runE(cmd *cobra.Command, args []string) error { + watchCfg, err := cmd.Flags().GetBool("watch") + if err != nil { + return err + } + + switch watchCfg { + case true: + return runWatch(cmd, args) + case false: + return runSingle(cmd, args) + } + + return nil +} + +func runSingle(_ *cobra.Command, args []string) error { + cfg, err := config.ReadConfig(args[0]) + if err != nil { + return err + } + + var dest string + if len(args) == 2 { + dest = args[1] + } + + return writeConfig(cfg, dest) +} + +func runWatch(cmd *cobra.Command, args []string) error { + // Initial output + err := runSingle(cmd, args) + if err != nil { + return err + } + + // Watcher output + configChannel, errorChannel := config.WatchConfig(cmd.Context(), args[0]) + + var dest string + if len(args) == 2 { + dest = args[1] + } + + waitFor := time.Millisecond * 100 + timer := time.NewTimer(waitFor) + <-timer.C + + var cachedConfig *domain.Config = nil + + for { + select { + case <-cmd.Context().Done(): + return nil + case err := <-errorChannel: + logrus.WithError(err).Errorln("Received from config watcher") + case <-timer.C: + writeConfig(cachedConfig, dest) + case config := <-configChannel: + cachedConfig = config + timer.Reset(waitFor) + } + } +} + +func writeConfig(config *domain.Config, dest string) error { + switch { + case len(dest) <= 0: + _, err := fmt.Fprintf(os.Stdout, "%s", config.API.Token) + if err != nil { + return err + } + case len(dest) > 0: + dirname := filepath.Dir(dest) + + err := os.MkdirAll(dirname, 0755) + if err != nil { + return err + } + + f, err := os.Create(dest) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + f.WriteString(config.API.Token) + } + + return nil +} diff --git a/cmd/healarr/LICENSE b/cmd/healarr/LICENSE new file mode 100644 index 0000000..b275dd7 --- /dev/null +++ b/cmd/healarr/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2024 Markus Pesch + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/cmd/healarr/Makefile b/cmd/healarr/Makefile new file mode 100644 index 0000000..4b92793 --- /dev/null +++ b/cmd/healarr/Makefile @@ -0,0 +1,37 @@ +VERSION?=$(shell git describe --abbrev=0)+hash.$(shell git rev-parse --short HEAD) + +# CONTAINER_RUNTIME +# The CONTAINER_RUNTIME variable will be used to specified the path to a +# container runtime. This is needed to start and run a container image. +CONTAINER_RUNTIME?=$(shell which podman) + +# BIN +# ============================================================================== +healarr: + CGO_ENABLED=0 \ + go build -ldflags "-X 'main.version=${VERSION}'" -o ${@} main.go + +# CLEAN +# ============================================================================== +PHONY+=clean +clean: + rm --force --recursive healarr + +# INSTALL +# ============================================================================== +PHONY+=install +install: healarr + install --directory ${DESTDIR}/etc/bash_completion.d + ./healarr completion bash > ${DESTDIR}/etc/bash_completion.d/healarr + + install --directory ${DESTDIR}${PREFIX}/bin + install --mode 0755 healarr ${DESTDIR}${PREFIX}/bin/healarr + + install --directory ${DESTDIR}${PREFIX}/share/licenses/healarr + install --mode 0644 LICENSE ${DESTDIR}${PREFIX}/share/licenses/healarr/LICENSE + +# PHONY +# ============================================================================== +# Declare the contents of the PHONY variable as phony. We keep that information +# in a variable so we can use it in if_changed. +.PHONY: ${PHONY} \ No newline at end of file diff --git a/cmd/healarr/main.go b/cmd/healarr/main.go new file mode 100644 index 0000000..bdba94f --- /dev/null +++ b/cmd/healarr/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "fmt" + "time" + + "git.cryptic.systems/volker.raschek/tarr/pkg/config" + "git.cryptic.systems/volker.raschek/tarr/pkg/domain" + "git.cryptic.systems/volker.raschek/tarr/pkg/health" + "github.com/spf13/cobra" +) + +var version string + +func main() { + bazarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr bazarr https://bazarr.example.com:8443 --config /etc/bazarr/config.yaml +healarr bazarr https://bazarr.example.com:8443 --api-token my-token`, + RunE: runBazarrE, + Short: "Check if a bazarr instance is healthy", + Use: `bazarr`, + } + + lidarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr lidarr https://lidarr.example.com:8443 --config /etc/lidarr/config.xml +healarr lidarr https://lidarr.example.com:8443 --api-token my-token`, + RunE: runLidarrE, + Short: "Check if a lidarr instance is healthy", + Use: `lidarr`, + } + + prowlarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr prowlarr https://prowlarr.example.com:8443 --config /etc/prowlarr/config.xml +healarr prowlarr https://prowlarr.example.com:8443 --api-token my-token`, + RunE: runProwlarrE, + Short: "Check if a prowlarr instance is healthy", + Use: `prowlarr`, + } + + radarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr radarr https://radarr.example.com:8443 --config /etc/radarr/config.xml +healarr radarr https://radarr.example.com:8443 --api-token my-token`, + RunE: runRadarrE, + Short: "Check if a radarr instance is healthy", + Use: `radarr`, + } + + readarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr readarr https://readarr.example.com:8443 --config /etc/readarr/config.xml +healarr readarr https://readarr.example.com:8443 --api-token my-token`, + RunE: runReadarrrE, + Short: "Check if a readarr instance is healthy", + Use: `readarr`, + } + + sabnzbdCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr sabnzbd https://sabnzbd.example.com:8443 --config /etc/sabnzbd/config.xml +healarr sabnzbd https://sabnzbd.example.com:8443 --api-token my-token`, + RunE: runSabNZBdE, + Short: "Check if a sabnzbd instance is healthy", + Use: `sabnzbd`, + } + + sonarrCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Example: `healarr sonarr https://sonarr.example.com:8443 --config /etc/sonarr/config.xml +healarr sonarr https://sonarr.example.com:8443 --api-token my-token`, + RunE: runSonarr, + Short: "Check if a sonarr instance is healthy", + Use: `sonarr`, + } + + rootCmd := &cobra.Command{ + Args: cobra.ExactArgs(1), + Long: `healarr exits with a non zero exit code, when the *arr application is not healthy`, + Version: version, + Use: "healarr", + } + rootCmd.AddCommand(bazarrCmd) + rootCmd.AddCommand(lidarrCmd) + rootCmd.AddCommand(prowlarrCmd) + rootCmd.AddCommand(radarrCmd) + rootCmd.AddCommand(readarrCmd) + rootCmd.AddCommand(sabnzbdCmd) + rootCmd.AddCommand(sonarrCmd) + rootCmd.PersistentFlags().String("api-token", "", "Token to access the *arr application") + rootCmd.PersistentFlags().String("config", "", "Path to an XML or YAML config file") + rootCmd.PersistentFlags().Bool("insecure", false, "Trust insecure TLS certificates") + rootCmd.PersistentFlags().Duration("timeout", time.Minute, "Timeout") + + rootCmd.Execute() +} + +func runBazarrE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.BazarrAPIQueryKeyAPIToken) +} + +func runLidarrE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.LidarrAPIQueryKeyAPIToken) +} + +func runProwlarrE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.ProwlarrAPIQueryKeyAPIToken) +} + +func runRadarrE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.RadarrAPIQueryKeyAPIToken) +} + +func runReadarrrE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.ReadarrAPIQueryKeyAPIToken) +} + +func runSabNZBdE(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.SabNZBdAPIQueryKeyAPIToken) +} + +func runSonarr(cmd *cobra.Command, args []string) error { + return runE(cmd, args, domain.SonarrAPIQueryKeyAPIToken) +} + +func runE(cmd *cobra.Command, args []string, queryKey string) error { + apiToken, err := cmd.Flags().GetString("api-token") + if err != nil { + return err + } + + configPath, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + insecure, err := cmd.Flags().GetBool("insecure") + if err != nil { + return err + } + + timeout, err := cmd.Flags().GetDuration("timeout") + if err != nil { + return err + } + + readinessProbeCtx, cancel := context.WithTimeout(cmd.Context(), timeout) + defer func() { cancel() }() + + switch { + case len(apiToken) <= 0 && len(configPath) <= 0: + return fmt.Errorf("At least --api-token oder --config must be defined") + case len(apiToken) > 0 && len(configPath) <= 0: + err = health.NewReadinessProbe(args[0]). + QueryAdd(queryKey, apiToken). + Insecure(insecure). + Run(readinessProbeCtx) + if err != nil { + return err + } + case len(apiToken) <= 0 && len(configPath) > 0: + config, err := config.ReadConfig(configPath) + if err != nil { + return err + } + + err = health.NewReadinessProbe(args[0]). + QueryAdd(queryKey, config.API.Token). + Insecure(insecure). + Run(readinessProbeCtx) + if err != nil { + return err + } + default: + return fmt.Errorf("Neither --api-token nor --config can be used at the same time.") + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..02a96ef --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.cryptic.systems/volker.raschek/tarr + +go 1.22.6 + +require ( + github.com/fsnotify/fsnotify v1.7.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d811c50 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/manifest.tmpl b/manifest.tmpl new file mode 100644 index 0000000..20d1020 --- /dev/null +++ b/manifest.tmpl @@ -0,0 +1,20 @@ +image: git.cryptic.systems/volker.raschek/tarr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} + - "latest" +{{/if}} +manifests: + - + image: git.cryptic.systems/volker.raschek/tarr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-amd64 + platform: + architecture: amd64 + os: linux + - + image: git.cryptic.systems/volker.raschek/tarr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-arm64-v8 + platform: + architecture: arm64 + os: linux + variant: v8 \ No newline at end of file diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..6b8b391 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,201 @@ +package config + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "os" + "strings" + "time" + + "git.cryptic.systems/volker.raschek/tarr/pkg/domain" + "github.com/fsnotify/fsnotify" + "gopkg.in/yaml.v3" +) + +type XMLConfig struct { + XMLName xml.Name `xml:"Config"` + APIToken string `xml:"ApiKey,omitempty"` + AuthenticationMethod string `xml:"AuthenticationMethod,omitempty"` + BindAddress string `xml:"BindAddress,omitempty"` + Branch string `xml:"Branch,omitempty"` + EnableSSL string `xml:"EnableSsl,omitempty"` + InstanceName string `xml:"InstanceName,omitempty"` + LaunchBrowser string `xml:"LaunchBrowser,omitempty"` + LogLevel string `xml:"LogLevel,omitempty"` + Port string `xml:"Port,omitempty"` + SSLCertPassword string `xml:"SSLCertPassword,omitempty"` + SSLCertPath string `xml:"SSLCertPath,omitempty"` + SSLPort string `xml:"SslPort,omitempty"` + UpdateMechanism string `xml:"UpdateMechanism,omitempty"` + URLBase string `xml:"UrlBase,omitempty"` +} + +type YAMLConfigAuth struct { + APIToken string `yaml:"apikey,omitempty"` + Password string `yaml:"password,omitempty"` + Type string `yaml:"type,omitempty"` + Username string `yaml:"username,omitempty"` +} + +type YAMLConfig struct { + Auth YAMLConfigAuth `yaml:"auth,omitempty"` +} + +// Read reads the config struct from a file. The decoding format will be determined by the file extension like +// `xml` or `yaml`. +func ReadConfig(name string) (*domain.Config, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + switch { + case strings.HasSuffix(name, "xml"): + return readXMLConfig(f) + case strings.HasSuffix(name, "yml") || strings.HasSuffix(name, "yaml"): + return readYAMLConfig(f) + default: + return nil, fmt.Errorf("Unsupported file extension") + } +} + +func readXMLConfig(r io.Reader) (*domain.Config, error) { + xmlConfig := new(XMLConfig) + + xmlDecoder := xml.NewDecoder(r) + err := xmlDecoder.Decode(xmlConfig) + if err != nil { + return nil, err + } + + return &domain.Config{ + API: &domain.API{ + Token: xmlConfig.APIToken, + }, + }, nil +} + +func readYAMLConfig(r io.Reader) (*domain.Config, error) { + yamlConfig := new(YAMLConfig) + + yamlDecoder := yaml.NewDecoder(r) + err := yamlDecoder.Decode(yamlConfig) + if err != nil { + return nil, err + } + + return &domain.Config{ + API: &domain.API{ + Password: yamlConfig.Auth.Password, + Token: yamlConfig.Auth.APIToken, + Username: yamlConfig.Auth.Username, + }, + }, nil +} + +func WatchConfig(ctx context.Context, name string) (<-chan *domain.Config, <-chan error) { + configChannel := make(chan *domain.Config, 0) + errorChannel := make(chan error, 0) + + go func() { + wait := time.Second * 3 + timer := time.NewTimer(wait) + <-timer.C + + watcher, err := fsnotify.NewWatcher() + if err != nil { + errorChannel <- err + return + } + watcher.Add(name) + + for { + select { + case <-ctx.Done(): + close(configChannel) + close(errorChannel) + break + case event, open := <-watcher.Events: + if !open { + errorChannel <- fmt.Errorf("FSWatcher closed channel: %w", err) + break + } + + switch event.Op { + case fsnotify.Write: + timer.Reset(wait) + } + case <-timer.C: + config, err := ReadConfig(name) + if err != nil { + errorChannel <- err + continue + } + configChannel <- config + } + } + }() + + return configChannel, errorChannel +} + +// WriteConfig writes the config struct into the file. The encoding format will be determined by the file extension like +// `xml` or `yaml`. +func WriteConfig(name string, config *domain.Config) error { + f, err := os.Create(name) + if err != nil { + return err + } + defer f.Close() + + switch { + case strings.HasSuffix(name, "xml"): + return writeXMLConfig(f, config) + case strings.HasSuffix(name, "yml") || strings.HasSuffix(name, "yaml"): + return writeYAMLConfig(f, config) + default: + return fmt.Errorf("Unsupported file extension") + } +} + +func writeXMLConfig(w io.Writer, config *domain.Config) error { + xmlEncoder := xml.NewEncoder(w) + defer xmlEncoder.Close() + + xmlConfig := &XMLConfig{ + APIToken: config.API.Token, + } + + xmlEncoder.Indent("", " ") + err := xmlEncoder.Encode(xmlConfig) + if err != nil { + return err + } + + return nil +} + +func writeYAMLConfig(w io.Writer, config *domain.Config) error { + yamlEncoder := yaml.NewEncoder(w) + defer yamlEncoder.Close() + + yamlConfig := &YAMLConfig{ + Auth: YAMLConfigAuth{ + APIToken: config.API.Token, + Password: config.API.Password, + Username: config.API.Username, + }, + } + + yamlEncoder.SetIndent(2) + + err := yamlEncoder.Encode(yamlConfig) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..3417fee --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,71 @@ +package config_test + +import ( + _ "embed" + "os" + "path/filepath" + "testing" + + "git.cryptic.systems/volker.raschek/tarr/pkg/config" + "github.com/stretchr/testify/require" +) + +//go:embed test/assets/xml/config.xml +var expectedXMLConfig string + +//go:embed test/assets/yaml/config.yaml +var expectedYAMLConfig string + +func TestReadWriteConfig_XML(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "*") + require.NoError(err) + t.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + + expectedXMLConfigName := filepath.Join(tmpDir, "expected_config.xml") + f, err := os.Create(expectedXMLConfigName) + require.NoError(err) + + _, err = f.WriteString(expectedXMLConfig) + require.NoError(err) + + actualConfig, err := config.ReadConfig(expectedXMLConfigName) + require.NoError(err) + require.NotNil(actualConfig) + + actualXMLConfigName := filepath.Join(tmpDir, "actual_config.xml") + err = config.WriteConfig(actualXMLConfigName, actualConfig) + require.NoError(err) + + b, err := os.ReadFile(actualXMLConfigName) + require.NoError(err) + require.Equal(expectedXMLConfig, string(b)) +} + +func TestReadWriteConfig_YAML(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "*") + require.NoError(err) + t.Cleanup(func() { _ = os.RemoveAll(tmpDir) }) + + expectedYAMLConfigName := filepath.Join(tmpDir, "expected_config.yaml") + f, err := os.Create(expectedYAMLConfigName) + require.NoError(err) + + _, err = f.WriteString(expectedYAMLConfig) + require.NoError(err) + + actualConfig, err := config.ReadConfig(expectedYAMLConfigName) + require.NoError(err) + require.NotNil(actualConfig) + + actualYAMLConfigName := filepath.Join(tmpDir, "actual_config.yaml") + err = config.WriteConfig(actualYAMLConfigName, actualConfig) + require.NoError(err) + + b, err := os.ReadFile(actualYAMLConfigName) + require.NoError(err) + require.Equal(expectedYAMLConfig, string(b)) +} diff --git a/pkg/config/test/assets/xml/config.xml b/pkg/config/test/assets/xml/config.xml new file mode 100644 index 0000000..cd95289 --- /dev/null +++ b/pkg/config/test/assets/xml/config.xml @@ -0,0 +1,3 @@ + + my-token + \ No newline at end of file diff --git a/pkg/config/test/assets/yaml/config.yaml b/pkg/config/test/assets/yaml/config.yaml new file mode 100644 index 0000000..bf86456 --- /dev/null +++ b/pkg/config/test/assets/yaml/config.yaml @@ -0,0 +1,4 @@ +auth: + apikey: my-token + password: my-password + username: my-username diff --git a/pkg/domain/bazarr.go b/pkg/domain/bazarr.go new file mode 100644 index 0000000..574cebe --- /dev/null +++ b/pkg/domain/bazarr.go @@ -0,0 +1,11 @@ +package domain + +const ( + BazarrAPIQueryKeyAPIToken string = "apikey" + LidarrAPIQueryKeyAPIToken string = "apiKey" + ProwlarrAPIQueryKeyAPIToken string = "apiKey" + RadarrAPIQueryKeyAPIToken string = "apiKey" + ReadarrAPIQueryKeyAPIToken string = "apiKey" + SabNZBdAPIQueryKeyAPIToken string = "apiKey" + SonarrAPIQueryKeyAPIToken string = "apiKey" +) diff --git a/pkg/domain/domain.go b/pkg/domain/domain.go new file mode 100644 index 0000000..c9a197f --- /dev/null +++ b/pkg/domain/domain.go @@ -0,0 +1,12 @@ +package domain + +type API struct { + Password string `yaml:"password"` + Token string `yaml:"token"` + URL string `yaml:"url"` + Username string `yaml:"username"` +} + +type Config struct { + API *API `yaml:"api"` +} diff --git a/pkg/health/health.go b/pkg/health/health.go new file mode 100644 index 0000000..e05ed27 --- /dev/null +++ b/pkg/health/health.go @@ -0,0 +1,78 @@ +package health + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "net/url" +) + +var ErrNoAPIToken error = errors.New("No API token defined") +var ErrNoURL error = errors.New("No API token defined") + +type ReadinessProbe struct { + additionalQueryValues url.Values + insecure bool + url string +} + +func (rp *ReadinessProbe) QueryAdd(key, value string) *ReadinessProbe { + if rp.additionalQueryValues.Has(key) { + rp.additionalQueryValues.Add(key, value) + } else { + rp.additionalQueryValues.Set(key, value) + } + return rp +} + +func (rp *ReadinessProbe) Insecure(insecure bool) *ReadinessProbe { + rp.insecure = insecure + return rp +} + +func (rp *ReadinessProbe) Run(ctx context.Context) error { + if len(rp.url) <= 0 { + return ErrNoURL + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: rp.insecure, + }, + }, + } + + req, err := http.NewRequest(http.MethodGet, rp.url, nil) + if err != nil { + return err + } + + reValues := req.URL.Query() + for key, values := range rp.additionalQueryValues { + for _, value := range values { + reValues.Add(key, value) + } + } + req.URL.RawQuery = reValues.Encode() + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Received unexpected HTTP status code %v", resp.StatusCode) + } + + return nil +} + +func NewReadinessProbe(url string) *ReadinessProbe { + return &ReadinessProbe{ + additionalQueryValues: make(map[string][]string), + url: url, + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..0ba4a0e --- /dev/null +++ b/renovate.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "assignees": [ + "volker.raschek" + ], + "customManagers": [ + { + "customType": "regex", + "description": "Update container-images in shell scripts", + "fileMatch": [ + "./Makefile" + ], + "matchStrings": [ + "^[^\\s]*VERSION(:|\\?)?=\"?(?[\\w.]*)\"? # renovate: datasource=(?[^\\s]*)( registryUrl=(?[^\\s]*))? depName=(?[^\\s]*)" + ] + } + ], + "labels": [ + "renovate" + ], + "packageRules": [ + { + "addLabels": [ + "renovate/automerge", + "renovate/container-image" + ], + "automerge": true, + "description": "Automatically update grouped public docker dependencies", + "enabled": true, + "groupName": "public container images", + "groupSlug": "public-container-images", + "matchDatasources": [ + "docker" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ] + }, + { + "description": "Automatically update patch versions of go modules", + "addLabels": [ + "renovate/gomod" + ], + "automerge": true, + "matchManagers": [ + "gomod" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ] + } + ], + "postUpdateOptions": [ + "gomodTidy" + ], + "rebaseLabel": "renovate/rebase", + "rebaseWhen": "behind-base-branch", + "rollbackPrs": true +} \ No newline at end of file