Initial Commit
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

This commit is contained in:
Markus Pesch 2024-08-18 20:49:30 +02:00
commit f79b20e8a4
Signed by: volker.raschek
GPG Key ID: 852BCC170D81A982
24 changed files with 1921 additions and 0 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
cmd/autharr/autharr
cmd/healarr/healarr

727
.drone.yml Normal file
View File

@ -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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
cmd/autharr/autharr
cmd/healarr/healarr

143
.markdownlint.yaml Normal file
View File

@ -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"

20
Dockerfile Normal file
View File

@ -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 /

20
LICENSE Normal file
View File

@ -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.

79
README.md Normal file
View File

@ -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
```

20
cmd/autharr/LICENSE Normal file
View File

@ -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.

37
cmd/autharr/Makefile Normal file
View File

@ -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}

123
cmd/autharr/main.go Normal file
View File

@ -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
}

20
cmd/healarr/LICENSE Normal file
View File

@ -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.

37
cmd/healarr/Makefile Normal file
View File

@ -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}

182
cmd/healarr/main.go Normal file
View File

@ -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
}

19
go.mod Normal file
View File

@ -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
)

29
go.sum Normal file
View File

@ -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=

20
manifest.tmpl Normal file
View File

@ -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

201
pkg/config/config.go Normal file
View File

@ -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
}

71
pkg/config/config_test.go Normal file
View File

@ -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))
}

View File

@ -0,0 +1,3 @@
<Config>
<ApiKey>my-token</ApiKey>
</Config>

View File

@ -0,0 +1,4 @@
auth:
apikey: my-token
password: my-password
username: my-username

11
pkg/domain/bazarr.go Normal file
View File

@ -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"
)

12
pkg/domain/domain.go Normal file
View File

@ -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"`
}

78
pkg/health/health.go Normal file
View File

@ -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,
}
}

61
renovate.json Normal file
View File

@ -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(:|\\?)?=\"?(?<currentValue>[\\w.]*)\"? # renovate: datasource=(?<datasource>[^\\s]*)( registryUrl=(?<registryUrl>[^\\s]*))? depName=(?<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
}