From f79b20e8a4f6399d680957b82de3181f60ce5200 Mon Sep 17 00:00:00 2001 From: Markus Pesch Date: Sun, 18 Aug 2024 20:49:30 +0200 Subject: [PATCH] Initial Commit --- .dockerignore | 2 + .drone.yml | 727 ++++++++++++++++++++++++ .gitignore | 2 + .markdownlint.yaml | 143 +++++ Dockerfile | 20 + LICENSE | 20 + README.md | 79 +++ cmd/autharr/LICENSE | 20 + cmd/autharr/Makefile | 37 ++ cmd/autharr/main.go | 123 ++++ cmd/healarr/LICENSE | 20 + cmd/healarr/Makefile | 37 ++ cmd/healarr/main.go | 182 ++++++ go.mod | 19 + go.sum | 29 + manifest.tmpl | 20 + pkg/config/config.go | 201 +++++++ pkg/config/config_test.go | 71 +++ pkg/config/test/assets/xml/config.xml | 3 + pkg/config/test/assets/yaml/config.yaml | 4 + pkg/domain/bazarr.go | 11 + pkg/domain/domain.go | 12 + pkg/health/health.go | 78 +++ renovate.json | 61 ++ 24 files changed, 1921 insertions(+) create mode 100644 .dockerignore create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 .markdownlint.yaml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/autharr/LICENSE create mode 100644 cmd/autharr/Makefile create mode 100644 cmd/autharr/main.go create mode 100644 cmd/healarr/LICENSE create mode 100644 cmd/healarr/Makefile create mode 100644 cmd/healarr/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 manifest.tmpl create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/test/assets/xml/config.xml create mode 100644 pkg/config/test/assets/yaml/config.yaml create mode 100644 pkg/domain/bazarr.go create mode 100644 pkg/domain/domain.go create mode 100644 pkg/health/health.go create mode 100644 renovate.json 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