Compare commits
51 commits
Author | SHA1 | Date | |
---|---|---|---|
3bc2cd20eb | |||
2b3d7f8289 | |||
d0312a5853 | |||
f066511eff | |||
ac5b9da086 | |||
a76d55c78b | |||
3d815758ba | |||
7db195af6d | |||
0f9ae11bbb | |||
67e9ce31b0 | |||
c5c6249fa7 | |||
61506f44f1 | |||
3ece069068 | |||
f37ea4fbbb | |||
20588b44bf | |||
4046695f1c | |||
abc78036d0 | |||
9259299a56 | |||
1fc3818d3c | |||
1d79258670 | |||
17592d4fad | |||
302d0b1ad4 | |||
d958f48717 | |||
9679166d0a | |||
47a48523a9 | |||
04a3ef39fa | |||
165b992629 | |||
95074b2a86 | |||
19c367a5d8 | |||
dce287d6a3 | |||
98b37ca289 | |||
c1631a0588 | |||
1406b53180 | |||
b20530941e | |||
52dfb5e21a | |||
7c3545976b | |||
c0928c4128 | |||
0277868c1b | |||
927bee30cf | |||
10bc3a59bb | |||
79c5da119a | |||
faffad851c | |||
f231e66e7c | |||
5d9fe621d8 | |||
66800d26df | |||
6420e19a8d | |||
e8dba980e1 | |||
99c63fdb8d | |||
5acc136fe8 | |||
0194429c80 | |||
feedfad99f |
|
@ -28,7 +28,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '^1.21'
|
go-version: '^1.22'
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Test native build
|
- name: Test native build
|
||||||
|
|
|
@ -3,9 +3,9 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
env:
|
env:
|
||||||
VERSION_MAJOR: 1
|
VERSION_MAJOR: 4
|
||||||
VERSION_MINOR: 0
|
VERSION_MINOR: 0
|
||||||
VERSION_PATCH: 3
|
VERSION_PATCH: 1
|
||||||
IMAGE_TAG: varakh/upda
|
IMAGE_TAG: varakh/upda
|
||||||
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda
|
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda
|
||||||
FORGEJO_URL: https://git.myservermanager.com
|
FORGEJO_URL: https://git.myservermanager.com
|
||||||
|
@ -33,7 +33,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '^1.21'
|
go-version: '^1.22'
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Test native build
|
- name: Test native build
|
||||||
|
|
69
CHANGELOG.md
|
@ -2,6 +2,65 @@
|
||||||
|
|
||||||
Changes adhere to [semantic versioning](https://semver.org).
|
Changes adhere to [semantic versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [4.0.1] - UNRELEASED
|
||||||
|
|
||||||
|
* ...
|
||||||
|
|
||||||
|
## [4.0.0] - 2024/10/25
|
||||||
|
|
||||||
|
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||||
|
|
||||||
|
* Embed frontend into Go binary and only ship _one_ OCI image
|
||||||
|
* Switch license to GPLv3
|
||||||
|
|
||||||
|
## [3.0.2] - 2024/06/15
|
||||||
|
|
||||||
|
* Don't enforce JSON content type for GET and DELETE requests
|
||||||
|
* Dependency updates
|
||||||
|
* github.com/go-playground/validator/v10 v10.22.0
|
||||||
|
* gorm.io/driver/postgres v1.5.9
|
||||||
|
* gorm.io/driver/sqlite v1.5.6
|
||||||
|
* Fixed filter for Updates ignoring desired state
|
||||||
|
|
||||||
|
## [3.0.1] - 2024/06/10
|
||||||
|
|
||||||
|
* Fixed finding proper remaining Action invocations by their state
|
||||||
|
|
||||||
|
## [3.0.0] - 2024/06/10
|
||||||
|
|
||||||
|
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||||
|
|
||||||
|
* Added automatic detection of `GOMAXPROCS`
|
||||||
|
* Switched to enforce JSON as `Content-Type` for all incoming requests
|
||||||
|
* Switched to properly respond with JSON on page not found or method not allowed
|
||||||
|
* Renamed `CORS_ALLOW_ORIGIN` to `CORS_ALLOW_ORIGINS`
|
||||||
|
* Added `CORS_ALLOW_CREDENTIALS` which defaults to `true`
|
||||||
|
* Added `CORS_EXPOSE_HEADERS` which defaults to `*`
|
||||||
|
* Overhauled package visibility for server module
|
||||||
|
* Updated dependencies
|
||||||
|
* Updated OCI image base to alpine `3.20` with Go `1.22`
|
||||||
|
|
||||||
|
## [2.0.1] - 2024/05/01
|
||||||
|
|
||||||
|
* Fixed retrieval of encrypted webhook token
|
||||||
|
|
||||||
|
## [2.0.0] - 2024/04/28
|
||||||
|
|
||||||
|
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||||
|
|
||||||
|
* Added _Actions_, a simple way to trigger notifications via [shoutrrr](https://containrrr.dev/shoutrrr) which supports
|
||||||
|
secrets
|
||||||
|
* Added new auth mode which allows setting multiple basic auth credentials
|
||||||
|
* Added `AUTH_MODE` which can be one of `basic_single` (_default_) and `basic_credentials`
|
||||||
|
* For `basic_credentials`: added `BASIC_AUTH_CREDENTIALS` which can be used as list of `username1=password1,...` (
|
||||||
|
comma separated)
|
||||||
|
* For `basic_single`: renamed `ADMIN_USER` and `ADMIN_PASSWORD` to `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD`
|
||||||
|
* Added mandatory `SECRET` environment variable to encrypt some data inside the database
|
||||||
|
* Switched to producing events only for _Updates_
|
||||||
|
* Switched to encrypting webhook tokens in database
|
||||||
|
* Adapted logging which defaults to JSON encoding
|
||||||
|
* Updated dependencies
|
||||||
|
|
||||||
## [1.0.3] - 2024/01/21
|
## [1.0.3] - 2024/01/21
|
||||||
|
|
||||||
* Updated dependencies
|
* Updated dependencies
|
||||||
|
@ -24,6 +83,16 @@ Changes adhere to [semantic versioning](https://semver.org).
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
||||||
|
[3.0.2]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.2
|
||||||
|
|
||||||
|
[3.0.1]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.1
|
||||||
|
|
||||||
|
[3.0.0]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.0
|
||||||
|
|
||||||
|
[2.0.1]: https://git.myservermanager.com/varakh/upda/releases/tag/2.0.1
|
||||||
|
|
||||||
|
[2.0.0]: https://git.myservermanager.com/varakh/upda/releases/tag/2.0.0
|
||||||
|
|
||||||
[1.0.3]: https://git.myservermanager.com/varakh/upda/releases/tag/1.0.3
|
[1.0.3]: https://git.myservermanager.com/varakh/upda/releases/tag/1.0.3
|
||||||
|
|
||||||
[1.0.2]: https://git.myservermanager.com/varakh/upda/releases/tag/1.0.2
|
[1.0.2]: https://git.myservermanager.com/varakh/upda/releases/tag/1.0.2
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
#
|
#
|
||||||
# Build image
|
# Build image
|
||||||
#
|
#
|
||||||
FROM alpine:3.18 AS builder
|
FROM alpine:3.20 AS builder
|
||||||
LABEL maintainer="Varakh <varakh@varakh.de>"
|
LABEL maintainer="Varakh <varakh@varakh.de>"
|
||||||
|
|
||||||
RUN apk --update upgrade && \
|
RUN apk --update upgrade && \
|
||||||
apk add go gcc make sqlite && \
|
apk add go gcc make sqlite && \
|
||||||
|
apk add nodejs npm && \
|
||||||
# See https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker
|
# See https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker
|
||||||
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
@ -18,7 +19,7 @@ RUN rm -rf bin/ && \
|
||||||
#
|
#
|
||||||
# Actual image
|
# Actual image
|
||||||
#
|
#
|
||||||
FROM alpine:3.18
|
FROM alpine:3.20
|
||||||
LABEL maintainer="Varakh <varakh@varakh.de>" \
|
LABEL maintainer="Varakh <varakh@varakh.de>" \
|
||||||
description="upda" \
|
description="upda" \
|
||||||
org.opencontainers.image.authors="Varakh" \
|
org.opencontainers.image.authors="Varakh" \
|
||||||
|
@ -26,7 +27,7 @@ LABEL maintainer="Varakh <varakh@varakh.de>" \
|
||||||
org.opencontainers.image.vendor="Varakh" \
|
org.opencontainers.image.vendor="Varakh" \
|
||||||
org.opencontainers.image.title="upda" \
|
org.opencontainers.image.title="upda" \
|
||||||
org.opencontainers.image.description="upda" \
|
org.opencontainers.image.description="upda" \
|
||||||
org.opencontainers.image.base.name="alpine:3.18"
|
org.opencontainers.image.base.name="alpine:3.20"
|
||||||
|
|
||||||
ENV USER=appuser \
|
ENV USER=appuser \
|
||||||
GROUP=appuser \
|
GROUP=appuser \
|
||||||
|
|
85
Makefile
|
@ -1,62 +1,87 @@
|
||||||
BIN_DIR = $(shell pwd)/bin
|
BIN_DIR = $(shell pwd)/bin
|
||||||
|
WEB_DIR = $(shell pwd)/server/web
|
||||||
|
WEB_BUILD_DIR = $(shell pwd)/server/web/build
|
||||||
|
WEB_NODE_DIR = $(shell pwd)/server/web/node_modules
|
||||||
|
WEB_COVERAGE_DIR = $(shell pwd)/server/web/coverage
|
||||||
|
|
||||||
clean:
|
# cleanup steps
|
||||||
|
clean: clean-server clean-web
|
||||||
|
clean-server:
|
||||||
rm -rf ${BIN_DIR}
|
rm -rf ${BIN_DIR}
|
||||||
|
clean-web:
|
||||||
|
rm -rf ${WEB_BUILD_DIR} ${WEB_NODE_DIR} ${WEB_COVERAGE_DIR}
|
||||||
|
|
||||||
dependencies:
|
# dependencies steps
|
||||||
|
dependencies: dependencies-web dependencies-server
|
||||||
|
dependencies-server:
|
||||||
GO111MODULE=on go mod download
|
GO111MODULE=on go mod download
|
||||||
|
dependencies-web:
|
||||||
|
cd ${WEB_DIR}; npm install
|
||||||
|
|
||||||
ci: clean dependencies test-ci build-server-ci build-cli-ci
|
# checkstyle steps
|
||||||
|
checkstyle: checkstyle-web checkstyle-server
|
||||||
|
checkstyle-server:
|
||||||
|
go vet ./...
|
||||||
|
checkstyle-web:
|
||||||
|
cd ${WEB_DIR}; npm run checkstyle
|
||||||
|
|
||||||
build-server-ci: build-server-linux-amd64
|
# test steps
|
||||||
|
test: test-web test-server
|
||||||
|
test-server:
|
||||||
|
go test -race ./...
|
||||||
|
test-web:
|
||||||
|
cd ${WEB_DIR}; npm run test:ci
|
||||||
|
|
||||||
|
# build steps
|
||||||
|
|
||||||
# server requires CGO_ENABLED=1 for go-sqlite
|
# server requires CGO_ENABLED=1 for go-sqlite
|
||||||
build-server-freebsd-amd64:
|
build-server-freebsd-amd64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-freebsd-amd64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-freebsd-amd64 cmd/server/main.go
|
||||||
build-server-freebsd-arm64:
|
build-server-freebsd-arm64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-freebsd-arm64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-freebsd-arm64 cmd/server/main.go
|
||||||
build-server-darwin-amd64:
|
build-server-darwin-amd64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-darwin-amd64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-darwin-amd64 cmd/server/main.go
|
||||||
build-server-darwin-arm64:
|
build-server-darwin-arm64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-darwin-arm64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-darwin-arm64 cmd/server/main.go
|
||||||
build-server-linux-amd64:
|
build-server-linux-amd64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-linux-amd64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-linux-amd64 cmd/server/main.go
|
||||||
build-server-linux-arm64:
|
build-server-linux-arm64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-linux-arm64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-linux-arm64 cmd/server/main.go
|
||||||
build-server-windows-amd64:
|
build-server-windows-amd64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-windows-amd64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-windows-amd64 cmd/server/main.go
|
||||||
build-server-windows-arm64:
|
build-server-windows-arm64:
|
||||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-windows-arm64 cmd/server.go
|
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-windows-arm64 cmd/server/main.go
|
||||||
|
|
||||||
# cli does not require CGO_ENABLED=1, cross-platform build possible
|
# cli does not require CGO_ENABLED=1, cross-platform build possible
|
||||||
build-cli-ci: build-cli-linux-amd64
|
|
||||||
|
|
||||||
build-cli-all: build-cli-freebsd-amd64 build-cli-freebsd-arm64 build-cli-darwin-amd64 build-cli-darwin-arm64 build-cli-linux-amd64 build-cli-linux-arm64 build-cli-windows-amd64 build-cli-windows-arm64
|
build-cli-all: build-cli-freebsd-amd64 build-cli-freebsd-arm64 build-cli-darwin-amd64 build-cli-darwin-arm64 build-cli-linux-amd64 build-cli-linux-arm64 build-cli-windows-amd64 build-cli-windows-arm64
|
||||||
|
|
||||||
build-cli-freebsd-amd64:
|
build-cli-freebsd-amd64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-freebsd-amd64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-freebsd-amd64 cmd/cli/main.go
|
||||||
build-cli-freebsd-arm64:
|
build-cli-freebsd-arm64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-freebsd-arm64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-freebsd-arm64 cmd/cli/main.go
|
||||||
build-cli-darwin-amd64:
|
build-cli-darwin-amd64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-darwin-amd64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-darwin-amd64 cmd/cli/main.go
|
||||||
build-cli-darwin-arm64:
|
build-cli-darwin-arm64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-darwin-arm64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-darwin-arm64 cmd/cli/main.go
|
||||||
build-cli-linux-amd64:
|
build-cli-linux-amd64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-linux-amd64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-linux-amd64 cmd/cli/main.go
|
||||||
build-cli-linux-arm64:
|
build-cli-linux-arm64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-linux-arm64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-linux-arm64 cmd/cli/main.go
|
||||||
build-cli-windows-amd64:
|
build-cli-windows-amd64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-windows-amd64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-windows-amd64 cmd/cli/main.go
|
||||||
build-cli-windows-arm64:
|
build-cli-windows-arm64:
|
||||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-windows-arm64 cmd/cli.go
|
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-windows-arm64 cmd/cli/main.go
|
||||||
|
|
||||||
test: test-server test-cli test-util
|
# remove built build/conf directory to be served live from the running binary
|
||||||
|
build-web:
|
||||||
|
cd ${WEB_DIR}; npm run build; rm -rf build/conf
|
||||||
|
|
||||||
|
# ci
|
||||||
|
clean-ci: clean
|
||||||
|
dependencies-ci: dependencies
|
||||||
|
checkstyle-ci: checkstyle
|
||||||
test-ci: test
|
test-ci: test
|
||||||
|
build-server-ci: build-server-linux-amd64
|
||||||
test-server:
|
build-cli-ci: build-cli-linux-amd64
|
||||||
GO111MODULE=on go test ./server/...
|
build-web-ci: build-web
|
||||||
test-cli:
|
ci: clean-ci dependencies-ci checkstyle-ci test-ci build-web-ci build-server-ci build-cli-ci
|
||||||
GO111MODULE=on go test ./terminal/...
|
|
||||||
test-util:
|
|
||||||
GO111MODULE=on go test ./util/...
|
|
||||||
|
|
340
README.md
|
@ -1,16 +1,6 @@
|
||||||
# README
|
# README
|
||||||
|
|
||||||
upda - **Up**date **Da**shboard in Go. Please see [motivation](#motivation) and [concepts](#concepts) what this
|
upda - **Up**date **Da**shboard in Go.
|
||||||
application does.
|
|
||||||
|
|
||||||
There's also a [upda web interface](https://git.myservermanager.com/varakh/upda-ui). It's recommended to take a look (at
|
|
||||||
least at the screenshots).
|
|
||||||
|
|
||||||
In addition, there's a commandline tool called `upda-cli`. For more information, download it and run `./upda-cli help`
|
|
||||||
for further instructions. This is especially useful, if you have an `upda` (server) running and like to invoke webhooks
|
|
||||||
from CLI. `upda-cli` is also bundled in the docker images.
|
|
||||||
|
|
||||||
**See the [deployment instructions](./_doc/DEPLOYMENT.md) for examples on how to deploy upda and upda-ui**
|
|
||||||
|
|
||||||
The main git repository is hosted at
|
The main git repository is hosted at
|
||||||
_[https://git.myservermanager.com/varakh/upda](https://git.myservermanager.com/varakh/upda)_.
|
_[https://git.myservermanager.com/varakh/upda](https://git.myservermanager.com/varakh/upda)_.
|
||||||
|
@ -18,271 +8,13 @@ Other repositories are mirrors and pull requests, issues, and planning are manag
|
||||||
|
|
||||||
Contributions are very welcome!
|
Contributions are very welcome!
|
||||||
|
|
||||||
* [Motivation](#motivation)
|
See [official documentation](./_doc/Home.md).
|
||||||
* [Concepts](#concepts)
|
|
||||||
* [Configuration](#configuration)
|
|
||||||
* [3rd party integrations](#3rd-party-integrations)
|
|
||||||
* [Webhooks](#webhooks)
|
|
||||||
* [Prometheus Metrics](#prometheus-metrics)
|
|
||||||
* [Deployment](#deployment)
|
|
||||||
* [Native](#native)
|
|
||||||
* [Docker](#docker)
|
|
||||||
* [Build docker image](#build-docker-image)
|
|
||||||
* [Development & contribution](#development--contribution)
|
|
||||||
* [Getting started](#getting-started)
|
|
||||||
* [Windows hints](#windows-hints)
|
|
||||||
* [Release](#release)
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
> [duin](https://crazymax.dev/diun/) can determine which OCI images have updates
|
|
||||||
> available. [Argus](https://release-argus.io) can query other sources like GitHub and even invoke actions when an
|
|
||||||
> update
|
|
||||||
> has been found, but there's no _convenient_ way of having **one** dashboard or source of truth for all of them across
|
|
||||||
> different hosts without tinkering with collecting them somewhere in one place. This application is the result of that
|
|
||||||
> tinkering. :-)
|
|
||||||
|
|
||||||
Managing various application or OCI container image updates can be a tedious task:
|
|
||||||
|
|
||||||
* A lot of hosts to operate with a lot of different applications being deployed
|
|
||||||
* A lot of different OCI containers to watch for updated images
|
|
||||||
* No convenient dashboard to see and manage all the available updates in one place
|
|
||||||
|
|
||||||
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to be called
|
|
||||||
via a webhook call (created within _upda_) from other applications, such as a bash script, an
|
|
||||||
application like [duin](https://crazymax.dev/diun/) or simply by using the `upda-cli`.
|
|
||||||
|
|
||||||
After an update is being tracked, _upda_ provides a convenient way to have everything in one place. In addition, it
|
|
||||||
exposes managed _updates_ as [prometheus](https://prometheus.io) metrics, so that you can easily build a dashboard
|
|
||||||
in [Grafana](https://grafana.com), or even attach alerts to pending updates
|
|
||||||
via [alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/).
|
|
||||||
|
|
||||||
In addition, you can use _upda_'s UI to manage updates, e.g. _approve_ them when they have been rolled out to a host.
|
|
||||||
|
|
||||||
Important to note:
|
|
||||||
|
|
||||||
* _upda_ is **NOT a scraper** to watch docker registries or GitHub releases, it simply collects and consolidates updates
|
|
||||||
from different sources via _webhooks_. If you like to watch GitHub releases, write a scraper and use `upda-cli` to
|
|
||||||
report back to _upda_.
|
|
||||||
* _upda_ uses basic auth for administrative tasks like viewing available updates or setting up the initial webhooks.
|
|
||||||
|
|
||||||
## Concepts
|
|
||||||
|
|
||||||
_upda_ retrieves new updates when webhooks of upda are invoked, e.g., [duin](https://crazymax.dev/diun/) invokes it
|
|
||||||
or any other application which can reach the instance.
|
|
||||||
Tracked updates are unique for the attributes `(application,provider,host)` which means that subsequent updates for an
|
|
||||||
identical _application_, _provider_ and _host_ simply updates the `version` and `metadata` attributes for that tracked
|
|
||||||
_update_ (regardless if the version or metadata payload _actually_ changed - reasoning behind this is to get reflected
|
|
||||||
metadata updates independent if version attribute has changed).
|
|
||||||
|
|
||||||
State management of tracked updates:
|
|
||||||
|
|
||||||
* On first creation, state is set to _pending_.
|
|
||||||
* When an _update_ is in _approved_ state, an invocation for it resets its state to _pending_.
|
|
||||||
* _Ignored_ updates are skipped entirely and no attribute is updated.
|
|
||||||
|
|
||||||
##### The `application` attribute
|
|
||||||
|
|
||||||
The _application_ attribute is an arbitrary identifier, name or label of a subject you like to track,
|
|
||||||
e.g., `docker.io/varakh/upda` for an OCI image.
|
|
||||||
|
|
||||||
##### The `provider` attribute
|
|
||||||
|
|
||||||
The _provider_ attribute is an arbitrary name or label. During webhook invocation the provider attribute is derived in
|
|
||||||
priority:
|
|
||||||
|
|
||||||
For the _generic_ webhook:
|
|
||||||
|
|
||||||
1. If the incoming payload contains a non-blank `provider` attribute, it's taken from the request.
|
|
||||||
2. If the incoming payload contains a blank or missing `provider` attribute, the issuing webhook's label is taken.
|
|
||||||
|
|
||||||
For the _diun_ webhook:
|
|
||||||
|
|
||||||
1. If the issuing webhook's label is blank, then `oci` is used.
|
|
||||||
2. In any other case, the webhook's label is used.
|
|
||||||
|
|
||||||
Because the first priority is the issuing webhook's label, setting the _same_ label for all webhooks results in a
|
|
||||||
grouping. Also see the _ignore host_ setting for `host` below.
|
|
||||||
|
|
||||||
_Remember that changing a webhook's label won't be reflected in already created/tracked updates!_
|
|
||||||
|
|
||||||
##### The `host` attribute
|
|
||||||
|
|
||||||
_host_ should be set to the originating host name a webhook has been issued from. The _host_
|
|
||||||
attribute can also be "ignored" (a setting in each webhook). If set to ignored, _upda_ sets _host_ to _global_, thus
|
|
||||||
update versions can be grouped independent of the originating host. If set for all webhooks, you'll end up with a host
|
|
||||||
independent update dashboard.
|
|
||||||
|
|
||||||
##### The `version` attribute
|
|
||||||
|
|
||||||
The _version_ attribute is an arbitrary name or label and subject to change across invocations of webhooks. This can be
|
|
||||||
a version number, a number of total updates, anything.
|
|
||||||
|
|
||||||
##### The `metadata` attribute
|
|
||||||
|
|
||||||
An update can hold any additional metadata information provided by request payload `metadata`. Metadata can be inspected
|
|
||||||
via web interface or API.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The following environment variables can be used to modify application behavior.
|
|
||||||
|
|
||||||
| Variable | Purpose | Default/Description |
|
|
||||||
|:-----------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
|
|
||||||
| `ADMIN_USER` | Admin user name for login | Not set by default, you need to explicitly set it to user name |
|
|
||||||
| `ADMIN_PASSWORD` | Admin password for login | Not set by default, you need to explicitly set it to a secure random |
|
|
||||||
| | | |
|
|
||||||
| `DB_TYPE` | The database type (Postgres is **recommended**) | Defaults to `sqlite`, possible values are `sqlite` or `postgres` |
|
|
||||||
| `DB_SQLITE_FILE` | Path to the SQLITE file | Defaults to `<XDG_DATA_DIR>/upda/upda.db`, e.g. `~/.local/share/upda/upda.db` |
|
|
||||||
| `DB_POSTGRES_HOST` | The postgres host | Postgres host address, defaults to `localhost` |
|
|
||||||
| `DB_POSTGRES_PORT` | The postgres port | Postgres port, defaults to `5432` |
|
|
||||||
| `DB_POSTGRES_NAME` | The postgres database name | Postgres database name, needs to be set |
|
|
||||||
| `DB_POSTGRES_TZ` | The postgres time zone | Postgres time zone settings, defaults to `Europe/Berlin` |
|
|
||||||
| `DB_POSTGRES_USER` | The postgres user | Postgres user name, needs to be set |
|
|
||||||
| `DB_POSTGRES_PASSWORD` | The postgres password | Postgres user password, needs to be set |
|
|
||||||
| | | |
|
|
||||||
| `SERVER_PORT` | Port | Defaults to `8080` |
|
|
||||||
| `SERVER_LISTEN` | Server's listen address | Defaults to empty which equals `0.0.0.0` |
|
|
||||||
| `SERVER_TLS_ENABLED` | If server uses TLS | Defaults `false` |
|
|
||||||
| `SERVER_TLS_CERT_PATH` | When TLS enabled, provide the certificate path | |
|
|
||||||
| `SERVER_TLS_KEY_PATH` | When TLS enabled, provide the key path | |
|
|
||||||
| `SERVER_TIMEOUT` | Timeout the server waits before shutting down to end any pending tasks | Defaults to `1s` (1 second), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| `CORS_ALLOW_ORIGIN` | CORS configuration | Defaults to `*` |
|
|
||||||
| `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
|
|
||||||
| `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` |
|
|
||||||
| | | |
|
|
||||||
| `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal` | Defaults to `info` |
|
|
||||||
| | | |
|
|
||||||
| `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number |
|
|
||||||
| | | |
|
|
||||||
| `TASK_UPDATE_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (ignored/approved) updates from the database | Defaults to `false` |
|
|
||||||
| `TASK_UPDATE_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (ignored/approved) updates from the database | Defaults to `1h` (1 hour), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| `TASK_UPDATE_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (ignored/approved) updates are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `168h` (168 hours = 1 week), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| `TASK_EVENT_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (old) events from the database | Defaults to `false` |
|
|
||||||
| `TASK_EVENT_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (old) events from the database | Defaults to `8h` (8 hours), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| `TASK_EVENT_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (old) events are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `2190h` (2190 hours = 3 months), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| `TASK_PROMETHEUS_REFRESH_INTERVAL` | Interval at which a background task updates custom metrics | Defaults to `60s` (60 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
|
||||||
| | | |
|
|
||||||
| `LOCK_REDIS_ENABLED` | If locking via REDIS (multiple instances) is enabled. Requires REDIS. Otherwise uses in-memory locks. | Defaults to `false` |
|
|
||||||
| `LOCK_REDIS_URL` | If locking via REDIS is enabled, this should point to a resolvable REDIS instance, e.g. `redis://<user>:<pass>@localhost:6379/<db>`. | |
|
|
||||||
| | | |
|
|
||||||
| `PROMETHEUS_ENABLED` | If Prometheus metrics are exposed | Defaults to `false` |
|
|
||||||
| `PROMETHEUS_METRICS_PATH` | Defines the metrics endpoint path | Defaults to `/metrics` |
|
|
||||||
| `PROMETHEUS_SECURE_TOKEN_ENABLED` | If Prometheus metrics endpoint is protected by a token when enabled (**recommended**) | Defaults to `true` |
|
|
||||||
| `PROMETHEUS_SECURE_TOKEN` | The token securing the metrics endpoint when enabled (**recommended**) | Not set by default, you need to explicitly set it to a secure random |
|
|
||||||
|
|
||||||
## 3rd party integrations
|
|
||||||
|
|
||||||
### Webhooks
|
|
||||||
|
|
||||||
This is the core mechanism of _upda_ and why it exists. Webhooks are the central piece of how _upda_ gets notified about
|
|
||||||
updates.
|
|
||||||
|
|
||||||
In order to configure a 3rd party application like [duin](https://crazymax.dev/diun/) to send updates to _upda_ with
|
|
||||||
the [duin webhook notification configuration](https://crazymax.dev/diun/notif/webhook/), **create** a new _upda_ webhook
|
|
||||||
token via _upda_'s web interface or via API call. This gives you
|
|
||||||
|
|
||||||
* a unique _upda_ URL to configure in the notification part of [duin](https://crazymax.dev/diun/),
|
|
||||||
e.g., `/api/v1/webhooks/<a unique identifier>`
|
|
||||||
* a corresponding token for the URL which must be sent as `X-Webhook-Token` header when calling _upda_'s URL
|
|
||||||
|
|
||||||
Expected payload is derived from the _type_ of the webhook which has been created in _upda_.
|
|
||||||
|
|
||||||
Example for [duin Webhook notification](https://crazymax.dev/diun/notif/webhook/) `notif`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
notif:
|
|
||||||
webhook:
|
|
||||||
endpoint: https://upda.domain.tld/api/v1/webhooks/ee03cd9e-04d0-4c7f-9866-efe219c2501e
|
|
||||||
method: POST
|
|
||||||
headers:
|
|
||||||
content-type: application/json
|
|
||||||
X-Webhook-Token: <the token from webhook creation in upda>
|
|
||||||
timeout: 10s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prometheus Metrics
|
|
||||||
|
|
||||||
When `PROMETHEUS_ENABLED` is set to `true`, default metrics about memory utilization, but also custom metrics specific
|
|
||||||
to _upda_ are exposed under the `PROMETHEUS_METRICS_PATH` endpoint.
|
|
||||||
|
|
||||||
A Prometheus scrape configuration might look like the following if `PROMETHEUS_SECURE_TOKEN_ENABLED` is set to `true`.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: 'upda'
|
|
||||||
static_configs:
|
|
||||||
- targets: ['upda:8080']
|
|
||||||
bearer_token: 'VALUE_OF_PROMETHEUS_SECURE_TOKEN'
|
|
||||||
```
|
|
||||||
|
|
||||||
Custom exposed metrics are exposed under the `upda_` namespace.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# HELP upda_updates details for all updates, -1=deleted (deleted next restart), 0=pending, 1=approved, 2=ignored
|
|
||||||
upda_updates{application="codeberg.org/forgejo/forgejo",host="myserver",provider="oci"} 0
|
|
||||||
upda_updates{application="docker.io/library/mysql",host="myserver",provider="oci"} 2
|
|
||||||
upda_updates{application="quay.io/navidys/prometheus-podman-exporter",host="myserver",provider="oci"} 1
|
|
||||||
upda_updates{application="quay.io/navidys/prometheus-podman-exporter",host="myserver2",provider="oci"} 1
|
|
||||||
# HELP upda_updates_all amount of all updates
|
|
||||||
upda_updates_all 4
|
|
||||||
# HELP upda_updates_approved amount of all updates in approved state
|
|
||||||
upda_updates_approved 2
|
|
||||||
# HELP upda_updates_ignored amount of all updates in ignored state
|
|
||||||
upda_updates_ignored 1
|
|
||||||
# HELP upda_updates_pending amount of all updates in pending state
|
|
||||||
upda_updates_pending 1
|
|
||||||
# HELP upda_webhooks amount of all webhooks
|
|
||||||
upda_webhooks 2
|
|
||||||
# HELP upda_events amount of all events
|
|
||||||
upda_events 146
|
|
||||||
```
|
|
||||||
|
|
||||||
There's an example [Grafana](https://grafana.com) dashboard in the `_doc/` folder.
|
|
||||||
|
|
||||||
[Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) could check for the following:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: update_checks
|
|
||||||
rules:
|
|
||||||
- alert: UpdatesAvailable
|
|
||||||
expr: upda_updates == 0 and upda_updates_pending > 0
|
|
||||||
for: 4w
|
|
||||||
labels:
|
|
||||||
severity: high
|
|
||||||
class: update
|
|
||||||
annotations:
|
|
||||||
summary: "Updates available from upda for {{ $labels.job }}"
|
|
||||||
description: "Updates available from upda for {{ $labels.job }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Native
|
|
||||||
|
|
||||||
Use the released binary for your platform or run `make clean build-server-{your-platform}` and the binary will be placed
|
|
||||||
into the `bin/` folder.
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
For examples how to run, look into [deployment instructions](./_doc/DEPLOYMENT.md) which contains examples
|
|
||||||
for `docker-compose` files.
|
|
||||||
|
|
||||||
#### Build docker image
|
|
||||||
|
|
||||||
To build docker images, do the following
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker build --rm --no-cache -t upda:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development & contribution
|
## Development & contribution
|
||||||
|
|
||||||
* Ensure to set `LOGGING_LEVEL=debug` for proper debug logs during development.
|
There's also a [embedded frontend](#embedded-frontend).
|
||||||
* Code guidelines
|
|
||||||
|
* Pay attention to `make checkstyle` (uses `go vet ./...`); pipeline fails if issues are detected.
|
||||||
* Each entity has its own repository
|
* Each entity has its own repository
|
||||||
* Each entity is only used in repository and service (otherwise, mapping happens)
|
* Each entity is only used in repository and service (otherwise, mapping happens)
|
||||||
* Presenter layer is constructed from the entity, e.g., in REST responses and mapped
|
* Presenter layer is constructed from the entity, e.g., in REST responses and mapped
|
||||||
|
@ -290,17 +22,41 @@ docker build --rm --no-cache -t upda:latest .
|
||||||
* All log calls should be handled by `zap.L()`
|
* All log calls should be handled by `zap.L()`
|
||||||
* Configuration is bootstrapped via separated `struct` types which are given to the service which need them
|
* Configuration is bootstrapped via separated `struct` types which are given to the service which need them
|
||||||
* Error handling
|
* Error handling
|
||||||
* Always throw an error with `NewServiceError`
|
* Always throw an error with `NewServiceError` for repositories, services and handlers
|
||||||
* Always wrap the cause error with `fmt.Errorf`
|
* Always throw an error wrapping the cause with `fmt.Errorf`
|
||||||
* Forward/bubble up the error directly, when original error is already a `NewServiceError` (most likely internal
|
* Forward/bubble up the error directly, when original error is already a `NewServiceError` (most likely internal
|
||||||
calls)
|
calls)
|
||||||
* Always abort handler chain with `AbortWithError`
|
* Always abort handler chain with `AbortWithError`
|
||||||
* Utils can throw any error
|
* Utils can throw any error
|
||||||
|
* Repositories, handlers and services should always properly return `error` including any `init`-like function (
|
||||||
|
best to avoid them and call in `newXXX`). **Do not abort with `Fatalf` or similar**
|
||||||
|
* `log.Fatalf` or `zap.L().Fatal` is allowed in `environment.go` or `app.go`
|
||||||
|
* Look into the `_doc/` folder for [OpenAPI specification](./_doc/api.yaml) and a Postman Collection.
|
||||||
|
* Consider reading [Effective Go](https://go.dev/doc/effective_go)
|
||||||
|
* Consider reading [100 Go Mistakes and How to Avoid Them](https://100go.co/)
|
||||||
|
|
||||||
Please look into the `_doc/` folder for [OpenAPI specification](./_doc/api.yaml) and a Postman Collection.
|
## Embedded Frontend
|
||||||
|
|
||||||
|
_upda_ includes a frontend in a monorepo fashion inside `server/web/`. For production (binary and OCI), it's
|
||||||
|
embedded into the GoLang binary itself.
|
||||||
|
|
||||||
|
For _development_, no other steps are required. Simply follow the [frontend instructions](./server/web/README.md) and
|
||||||
|
start the frontend separately.
|
||||||
|
|
||||||
|
If you like to have a look on the _production_ experience, the frontend needs to be build first and you need to build
|
||||||
|
the Golang binary with `-tags prod`. How to properly build the frontend, please look into `build-web` of
|
||||||
|
the `Makefile` (additional `rm -rf` cmd).
|
||||||
|
|
||||||
### Getting started
|
### Getting started
|
||||||
|
|
||||||
|
Ensure to set the following environment variables for proper debug logs during development
|
||||||
|
|
||||||
|
```shell
|
||||||
|
DEVELOPMENT=true
|
||||||
|
LOGGING_ENCODING=console
|
||||||
|
LOGGING_LEVEL=debug
|
||||||
|
```
|
||||||
|
|
||||||
1. Run `make clean dependencies` to fetch dependencies
|
1. Run `make clean dependencies` to fetch dependencies
|
||||||
2. Start `git.myservermanager.com/varakh/upda/cmd/server` (or `cli`) as Go application and ensure to have _required_
|
2. Start `git.myservermanager.com/varakh/upda/cmd/server` (or `cli`) as Go application and ensure to have _required_
|
||||||
environment variables set
|
environment variables set
|
||||||
|
@ -332,6 +88,34 @@ path.
|
||||||
For any `go` command you run, ensure that your `PATH` has the `gcc` binary and that you add `CGO_ENABLED=1` as
|
For any `go` command you run, ensure that your `PATH` has the `gcc` binary and that you add `CGO_ENABLED=1` as
|
||||||
environment.
|
environment.
|
||||||
|
|
||||||
|
### Using the `lockService` correctly
|
||||||
|
|
||||||
|
The `lockService` can be used to lock resources. This works in-memory and also in a distributed fashion with REDIS.
|
||||||
|
|
||||||
|
Ensure to provide proper locking options when using, although in-memory ignores those.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# invoked from an endpoint
|
||||||
|
context := c.Request.Context()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var lock appLock
|
||||||
|
|
||||||
|
if lock, err = h.lockService.lockWithOptions(context, "TEST-LOCK", withAppLockOptionExpiry(5*time.Minute), withAppLockOptionInfiniteRetries(), withAppLockOptionRetryDelay(5*time.Second)); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# defer to avoid leakage
|
||||||
|
defer func(lock appLock) {
|
||||||
|
_ = lock.unlock(context)
|
||||||
|
}(lock)
|
||||||
|
|
||||||
|
# simulate long running task
|
||||||
|
time.Sleep(20 * time.Second)
|
||||||
|
```
|
||||||
|
|
||||||
### Release
|
### Release
|
||||||
|
|
||||||
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
||||||
|
@ -339,7 +123,7 @@ platform, uploads produced artifacts to that release and publishes docker images
|
||||||
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
||||||
built successfully:
|
built successfully:
|
||||||
|
|
||||||
* Adapt `constants_app.go` and change `Version` to the correct version number
|
* Adapt `commons/constants.go` and change `Version` to the correct version number
|
||||||
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||||
in footer (link to scm git tag source)
|
in footer (link to scm git tag source)
|
||||||
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
||||||
|
@ -347,7 +131,7 @@ built successfully:
|
||||||
|
|
||||||
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||||
|
|
||||||
* Adapt `constants_app.go` and change `Version` to the _next_ version number
|
* Adapt `commons/constants.go` and change `Version` to the _next_ version number
|
||||||
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||||
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
||||||
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
||||||
|
|
63
_doc/Concepts.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# Concepts, a deeper dive
|
||||||
|
|
||||||
|
The following section goes into a deeper look into upda's internals.
|
||||||
|
|
||||||
|
1. Create a webhook in upda.
|
||||||
|
2. Use the webhook's URL in a 3rd party application to start tracking an update or use `upda-cli` to report an update.
|
||||||
|
3. Enjoy visualization and state management of tracked updates in one place.
|
||||||
|
4. Optionally, define _Actions_ for tracked updates as they arrive
|
||||||
|
|
||||||
|
_upda_ retrieves new updates when webhooks of upda are invoked, e.g., [duin](https://crazymax.dev/diun/) invokes it or
|
||||||
|
any other application which can reach the instance. Tracked updates are unique for the
|
||||||
|
attributes `(application,provider,host)` which means that subsequent updates for an identical _application_, _provider_
|
||||||
|
and _host_ simply updates the `version` and `metadata` attributes for that tracked _update_ (regardless if the version
|
||||||
|
or metadata payload _actually_ changed - reasoning behind this is to get reflected metadata updates independent if
|
||||||
|
version attribute has changed).
|
||||||
|
|
||||||
|
State management of tracked updates:
|
||||||
|
|
||||||
|
* On first creation, state is set to _pending_.
|
||||||
|
* When an _update_ is in _approved_ state, an invocation for it resets its state to _pending_.
|
||||||
|
* _Ignored_ updates are skipped entirely and no attribute is updated.
|
||||||
|
|
||||||
|
##### The `application` attribute
|
||||||
|
|
||||||
|
The _application_ attribute is an arbitrary identifier, name or label of a subject you like to track,
|
||||||
|
e.g., `docker.io/varakh/upda` for an OCI image.
|
||||||
|
|
||||||
|
##### The `provider` attribute
|
||||||
|
|
||||||
|
The _provider_ attribute is an arbitrary name or label. During webhook invocation the provider attribute is derived in
|
||||||
|
priority:
|
||||||
|
|
||||||
|
For the _generic_ webhook:
|
||||||
|
|
||||||
|
1. If the incoming payload contains a non-blank `provider` attribute, it's taken from the request.
|
||||||
|
2. If the incoming payload contains a blank or missing `provider` attribute, the issuing webhook's label is taken.
|
||||||
|
|
||||||
|
For the _diun_ webhook:
|
||||||
|
|
||||||
|
1. If the issuing webhook's label is blank, then `oci` is used.
|
||||||
|
2. In any other case, the webhook's label is used.
|
||||||
|
|
||||||
|
Because the first priority is the issuing webhook's label, setting the _same_ label for all webhooks results in a
|
||||||
|
grouping. Also see the _ignore host_ setting for `host` below.
|
||||||
|
|
||||||
|
_Remember that changing a webhook's label won't be reflected in already created/tracked updates!_
|
||||||
|
|
||||||
|
##### The `host` attribute
|
||||||
|
|
||||||
|
_host_ should be set to the originating host name a webhook has been issued from. The _host_
|
||||||
|
attribute can also be "ignored" (a setting in each webhook). If set to ignored, _upda_ sets _host_ to _global_, thus
|
||||||
|
update versions can be grouped independent of the originating host. If set for all webhooks, you'll end up with a host
|
||||||
|
independent update dashboard.
|
||||||
|
|
||||||
|
##### The `version` attribute
|
||||||
|
|
||||||
|
The _version_ attribute is an arbitrary name or label and subject to change across invocations of webhooks. This can be
|
||||||
|
a version number, a number of total updates, anything.
|
||||||
|
|
||||||
|
##### The `metadata` attribute
|
||||||
|
|
||||||
|
An update can hold any additional metadata information provided by request payload `metadata`. Metadata can be inspected
|
||||||
|
via web interface or API.
|
74
_doc/Configuration.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
The following table describe available configuration values.
|
||||||
|
|
||||||
|
| Variable | Purpose | Default/Description |
|
||||||
|
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `SECRET` | A 32 character long secure random secret used for encrypting some data inside the database. When data has been created inside the database, the secret cannot be changed anymore, otherwise decryption fails. | Not set by default, you need to explicitly set it, e.g., generate via `openssl rand -hex 16` |
|
||||||
|
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
|
||||||
|
| | | |
|
||||||
|
| `AUTH_MODE` | The auth mode. Possible values are `basic_single` and `basic_credentials` | Defaults to `basic_single` |
|
||||||
|
| `BASIC_AUTH_USER` | For auth mode `basic_single`: Username for login | Not set by default, you need to explicitly set it to user name |
|
||||||
|
| `BASIC_AUTH_PASSWORD` | For auth mode `basic_single`: User's password for login | Not set by default, you need to explicitly set it to a secure random |
|
||||||
|
| `BASIC_AUTH_CREDENTIALS` | For auth mode `basic_credentials`: list of comma separated credentials, e.g. `username1=password1,username2=password2` | Not set by default, you need to explicitly set it |
|
||||||
|
| | | |
|
||||||
|
| `DB_TYPE` | The database type (Postgres is **recommended**) | Defaults to `sqlite`, possible values are `sqlite` or `postgres` |
|
||||||
|
| `DB_SQLITE_FILE` | Path to the SQLITE file | Defaults to `<XDG_DATA_DIR>/upda/upda.db`, e.g. `~/.local/share/upda/upda.db` |
|
||||||
|
| `DB_POSTGRES_HOST` | The postgres host | Postgres host address, defaults to `localhost` |
|
||||||
|
| `DB_POSTGRES_PORT` | The postgres port | Postgres port, defaults to `5432` |
|
||||||
|
| `DB_POSTGRES_NAME` | The postgres database name | Postgres database name, needs to be set |
|
||||||
|
| `DB_POSTGRES_TZ` | The postgres time zone | Postgres time zone settings, defaults to `Europe/Berlin` |
|
||||||
|
| `DB_POSTGRES_USER` | The postgres user | Postgres user name, needs to be set |
|
||||||
|
| `DB_POSTGRES_PASSWORD` | The postgres password | Postgres user password, needs to be set |
|
||||||
|
| | | |
|
||||||
|
| `SERVER_PORT` | Port | Defaults to `8080` |
|
||||||
|
| `SERVER_LISTEN` | Server's listen address | Defaults to empty which equals `0.0.0.0` |
|
||||||
|
| `SERVER_TLS_ENABLED` | If server uses TLS | Defaults `false` |
|
||||||
|
| `SERVER_TLS_CERT_PATH` | When TLS enabled, provide the certificate path | |
|
||||||
|
| `SERVER_TLS_KEY_PATH` | When TLS enabled, provide the key path | |
|
||||||
|
| `SERVER_TIMEOUT` | Timeout the server waits before shutting down to end any pending tasks | Defaults to `1s` (1 second), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `CORS_ALLOW_ORIGINS` | CORS configuration | Defaults to `*` |
|
||||||
|
| `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
|
||||||
|
| `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` |
|
||||||
|
| `CORS_ALLOW_CREDENTIALS` | CORS configuration | Defaults to `true` |
|
||||||
|
| `CORS_EXPOSE_HEADERS` | CORS configuration | Defaults to `*` |
|
||||||
|
| | | |
|
||||||
|
| `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal`. Setting to `debug` enables high verbosity output. | Defaults to `info` |
|
||||||
|
| `LOGGING_ENCODING` | Logging encoding. Possible are `console` and `json` | Defaults to `json` |
|
||||||
|
| `LOGGING_DIRECTORY` | Logging directory. When set, logs will be added to a file called `upda.log` in addition to the standard output. Ensure that upda has access permissions. Use an external program for log rotation if desired. | |
|
||||||
|
| | | |
|
||||||
|
| `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_UPDATE_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (ignored/approved) updates from the database | Defaults to `false` |
|
||||||
|
| `TASK_UPDATE_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (ignored/approved) updates from the database | Defaults to `1h` (1 hour), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `TASK_UPDATE_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (ignored/approved) updates are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `720h` (168 hours = 1 week), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_EVENT_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (old) events from the database | Defaults to `false` |
|
||||||
|
| `TASK_EVENT_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (old) events from the database | Defaults to `8h` (8 hours), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `TASK_EVENT_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (old) events are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `2190h` (2190 hours = 3 months), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_ACTIONS_ENQUEUE_ENABLED` | If background task should run to enqueue matching actions derived from events (actions are invocation separately after being enqueued) | Defaults to `true` |
|
||||||
|
| `TASK_ACTIONS_ENQUEUE_INTERVAL` | Interval at which a background task does check to enqueue actions | Defaults to `10s` (10 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `TASK_ACTIONS_ENQUEUE_BATCH_SIZE` | Number defining how many unhandled events are processed in a batch by the background task | Defaults to `1`, must be positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_ACTIONS_INVOKE_ENABLED` | If background task should run to invoke enqueued actions derived | Defaults to `true` |
|
||||||
|
| `TASK_ACTIONS_INVOKE_INTERVAL` | Interval at which a background task does check to invoke enqueued actions | Defaults to `10s` (10 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `TASK_ACTIONS_INVOKE_BATCH_SIZE` | Number defining how many enqueued actions are processed in a batch by the background task | Defaults to `1`, must be positive number |
|
||||||
|
| `TASK_ACTIONS_INVOKE_MAX_RETRIES` | Number defining how often actions are invoked in case of an error, if exceeded, those actions are not retried again | Defaults to `3`, must be positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_ACTIONS_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (handled, meaning success or error state) actions from the database | Defaults to `true` |
|
||||||
|
| `TASK_ACTIONS_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (handled) actions from the database | Defaults to `12h` (12 hours), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `TASK_ACTIONS_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (handled) actions are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `720h` (720 hours = 30 days), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_PROMETHEUS_REFRESH_INTERVAL` | Interval at which a background task updates custom metrics | Defaults to `60s` (60 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| | | |
|
||||||
|
| `LOCK_REDIS_ENABLED` | If locking via REDIS (multiple instances) is enabled. Requires REDIS. Otherwise uses in-memory locks. | Defaults to `false` |
|
||||||
|
| `LOCK_REDIS_URL` | If locking via REDIS is enabled, this should point to a resolvable REDIS instance, e.g. `redis://<user>:<pass>@localhost:6379/<db>`. | |
|
||||||
|
| | | |
|
||||||
|
| `PROMETHEUS_ENABLED` | If Prometheus metrics are exposed | Defaults to `false` |
|
||||||
|
| `PROMETHEUS_METRICS_PATH` | Defines the metrics endpoint path | Defaults to `/metrics` |
|
||||||
|
| `PROMETHEUS_SECURE_TOKEN_ENABLED` | If Prometheus metrics endpoint is protected by a token when enabled (**recommended**) | Defaults to `true` |
|
||||||
|
| `PROMETHEUS_SECURE_TOKEN` | The token securing the metrics endpoint when enabled (**recommended**) | Not set by default, you need to explicitly set it to a secure random |
|
||||||
|
| | | |
|
||||||
|
| `WEB_API_URL` | Base URL of API, e.g. `https://upda.domain.tld` | `http://localhost` |
|
||||||
|
| `WEB_TITLE` | The title of the frontend page | `upda` |
|
|
@ -1,170 +0,0 @@
|
||||||
# Deployment
|
|
||||||
|
|
||||||
Use one of the provided `docker-compose` examples, edit to your needs. Then issue `docker compose up` command.
|
|
||||||
|
|
||||||
All applications should be up and running.
|
|
||||||
|
|
||||||
As of now, the web interface and the server comes as different container images.
|
|
||||||
|
|
||||||
Default image user is `appuser` (`uid=2033`) and group is `appgroup` (`gid=2033`).
|
|
||||||
|
|
||||||
The following examples are available
|
|
||||||
|
|
||||||
## Postgres
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.9'
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
external: false
|
|
||||||
driver: bridge
|
|
||||||
driver_opts:
|
|
||||||
com.docker.network.bridge.name: br-upda
|
|
||||||
|
|
||||||
services:
|
|
||||||
ui:
|
|
||||||
container_name: upda_ui
|
|
||||||
image: git.myservermanager.com/varakh/upda-ui:latest
|
|
||||||
environment:
|
|
||||||
- VITE_API_URL=https://upda.domain.tld/api/v1/
|
|
||||||
- VITE_APP_TITLE=upda
|
|
||||||
- VITE_APP_DESCRIPTION=upda
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8181:80"
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
|
|
||||||
api:
|
|
||||||
container_name: upda_api
|
|
||||||
image: git.myservermanager.com/varakh/upda:latest
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- DB_POSTGRES_TZ=Europe/Berlin
|
|
||||||
- DB_TYPE=postgres
|
|
||||||
- DB_POSTGRES_HOST=db
|
|
||||||
- DB_POSTGRES_PORT=5432
|
|
||||||
- DB_POSTGRES_NAME=upda
|
|
||||||
- DB_POSTGRES_USER=upda
|
|
||||||
- DB_POSTGRES_PASSWORD=upda
|
|
||||||
- ADMIN_USER=admin
|
|
||||||
- ADMIN_PASSWORD=changeit
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8080:8080"
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
db:
|
|
||||||
container_name: upda_db
|
|
||||||
image: postgres:16
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=upda
|
|
||||||
- POSTGRES_PASSWORD=upda
|
|
||||||
- POSTGRES_DB=upda
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
volumes:
|
|
||||||
- upda-db-vol:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
upda-db-vol:
|
|
||||||
external: false
|
|
||||||
```
|
|
||||||
|
|
||||||
## SQLite
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.9'
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
external: false
|
|
||||||
driver: bridge
|
|
||||||
driver_opts:
|
|
||||||
com.docker.network.bridge.name: br-upda
|
|
||||||
|
|
||||||
services:
|
|
||||||
ui:
|
|
||||||
container_name: upda_ui
|
|
||||||
image: git.myservermanager.com/varakh/upda-ui:latest
|
|
||||||
environment:
|
|
||||||
- VITE_API_URL=https://upda.domain.tld/api/v1/
|
|
||||||
- VITE_APP_TITLE=upda
|
|
||||||
- VITE_APP_DESCRIPTION=upda
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8181:80"
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
|
|
||||||
api:
|
|
||||||
container_name: upda_api
|
|
||||||
image: git.myservermanager.com/varakh/upda:latest
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- DB_SQLITE_FILE=/data/upda.db
|
|
||||||
- ADMIN_USER=admin
|
|
||||||
- ADMIN_PASSWORD=changeit
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
volumes:
|
|
||||||
- upda-app-vol:/data
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8080:8080"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
upda-app-vol:
|
|
||||||
external: false
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reverse proxy
|
|
||||||
|
|
||||||
You may want to use a proxy in front of them on your host, e.g., nginx. Here's a configuration snippet which should do
|
|
||||||
the work.
|
|
||||||
|
|
||||||
The UI and API is reachable through the same domain, e.g., `https://upda.domain.tld`. In addition, Let's Encrypt is used
|
|
||||||
for transport encryption.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
ssl_certificate /etc/letsencrypt/live/upda.domain.tld/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/upda.domain.tld/privkey.pem;
|
|
||||||
|
|
||||||
# ui
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:8181;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# api
|
|
||||||
location ~* ^/(api)/ {
|
|
||||||
proxy_pass http://localhost:8080;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
# metrics
|
|
||||||
location ~* ^/metrics {
|
|
||||||
proxy_pass http://localhost:8080;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
228
_doc/Deployment.md
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
# Deployment
|
||||||
|
|
||||||
|
_upda_ is a server application which embeds a webinterface directly in its binary form. This makes it easy to deploy
|
||||||
|
natively. In addition, a _upda_ docker image is provided to get started quickly.
|
||||||
|
|
||||||
|
_upda-cli_ which is an optional commandline helper to quickly invoke webhooks or list tracked updates in
|
||||||
|
your is also embedded into the docker image, but can also be downloaded for your operating system.
|
||||||
|
|
||||||
|
The following sections outline how to deploy _upda_ in a containerized environment and also natively.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
|
Use one of the provided `docker-compose` examples, edit to your needs. Then issue `docker compose up -d` command and
|
||||||
|
`docker compose logs -f` to trace the log.
|
||||||
|
|
||||||
|
Default image user is `appuser` (`uid=2033`) and group is `appgroup` (`gid=2033`).
|
||||||
|
|
||||||
|
The following examples are available
|
||||||
|
|
||||||
|
### Postgres
|
||||||
|
|
||||||
|
#### docker-compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
external: false
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: br-upda
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: upda_app
|
||||||
|
image: git.myservermanager.com/varakh/upda:latest
|
||||||
|
environment:
|
||||||
|
- WEB_API_URL=https://upda.domain.tld
|
||||||
|
- WEB_TITLE=upda
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- DB_POSTGRES_TZ=Europe/Berlin
|
||||||
|
- DB_TYPE=postgres
|
||||||
|
- DB_POSTGRES_HOST=db
|
||||||
|
- DB_POSTGRES_PORT=5432
|
||||||
|
- DB_POSTGRES_NAME=upda
|
||||||
|
- DB_POSTGRES_USER=upda
|
||||||
|
- DB_POSTGRES_PASSWORD=upda
|
||||||
|
- BASIC_AUTH_USER=admin
|
||||||
|
- BASIC_AUTH_PASSWORD=changeit
|
||||||
|
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||||
|
- SECRET=generated-secure-secret-32-chars
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: upda_db
|
||||||
|
image: postgres:16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=upda
|
||||||
|
- POSTGRES_PASSWORD=upda
|
||||||
|
- POSTGRES_DB=upda
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- upda-db-vol:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
upda-db-vol:
|
||||||
|
external: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
#### docker-compose
|
||||||
|
|
||||||
|
You can use the following to get it up running quickly via docker compose.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
external: false
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: br-upda
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: upda_app
|
||||||
|
image: git.myservermanager.com/varakh/upda:latest
|
||||||
|
environment:
|
||||||
|
- WEB_API_URL=https://upda.domain.tld
|
||||||
|
- WEB_TITLE=upda
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- BASIC_AUTH_USER=admin
|
||||||
|
- BASIC_AUTH_PASSWORD=changeit
|
||||||
|
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||||
|
- SECRET=generated-secure-secret-32-chars
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- upda-app-vol:/home/appuser
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
upda-app-vol:
|
||||||
|
external: false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Local example
|
||||||
|
|
||||||
|
For spinning it up **locally** and without a [reverse proxy](#reverse-proxy), you can use the following simple `docker`
|
||||||
|
commands.
|
||||||
|
|
||||||
|
Make sure to adapt `DOMAIN` and pipe in your device IP address (LAN), e.g., `192.168.1.42`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# create volume
|
||||||
|
docker volume create upda-app-vol
|
||||||
|
|
||||||
|
# run locally binding to your LAN IP address
|
||||||
|
docker run --name upda_app \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e TZ=Europe/Berlin \
|
||||||
|
-e WEB_API_URL=http://192.168.1.42:8080 \
|
||||||
|
-e BASIC_AUTH_USER=admin \
|
||||||
|
-e BASIC_AUTH_PASSWORD=changeit \
|
||||||
|
-v upda-app-vol:/home/appuser \
|
||||||
|
varakh/upda:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## High availability
|
||||||
|
|
||||||
|
For high availability, pick the [Postgres setup](#postgres) and add [REDIS](https://redis.io/) to support proper
|
||||||
|
distributed locking.
|
||||||
|
|
||||||
|
Make changes to your docker-compose deployment similar to the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# the existing app service - add these changes to all instances, so they all use the same redis instance
|
||||||
|
# make sure that all of them can connect to the redis instance
|
||||||
|
# ...
|
||||||
|
app:
|
||||||
|
environment:
|
||||||
|
- LOCK_REDIS_ENABLED=true
|
||||||
|
- LOCK_REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# the new redis service
|
||||||
|
redis:
|
||||||
|
container_name: upda_redis
|
||||||
|
image: redis
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- redis-data-vol:/var/redis/data
|
||||||
|
# optionally expose port depending on your setup
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:6379:6379"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# other already defined volumes
|
||||||
|
# ...
|
||||||
|
redis-data-vol:
|
||||||
|
external: false
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, you need a proper load balancer which routes incoming traffic to all of your instances.
|
||||||
|
|
||||||
|
Furthermore, you can also decide to have the frontend in a high-availability setup.
|
||||||
|
|
||||||
|
## Reverse proxy
|
||||||
|
|
||||||
|
You may want to use a proxy in front of them on your host, e.g., nginx. Here's a configuration snippet which should do
|
||||||
|
the work.
|
||||||
|
|
||||||
|
The UI and API (backend/server) is reachable through the same domain, e.g., `https://upda.domain.tld`. In addition,
|
||||||
|
Let's Encrypt is used for transport encryption.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/upda.domain.tld/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/upda.domain.tld/privkey.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Native
|
||||||
|
|
||||||
|
Native deployment is also possible.
|
||||||
|
|
||||||
|
Download the binary for your operating system. Next, use the binary or execute it locally.
|
||||||
|
|
||||||
|
See the provided systemd service example `upda.service` to deploy on a UNIX/Linux machine.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
[Unit]
|
||||||
|
Description=upda
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# Using a dynamic user drops privileges and sets some security defaults
|
||||||
|
# See https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
|
||||||
|
DynamicUser=yes
|
||||||
|
# All environment variables for upda can be put into this file
|
||||||
|
# upda picks them up (on each restart)
|
||||||
|
EnvironmentFile=/etc/upda.conf
|
||||||
|
# Requires upda' binary to be installed at this location, e.g., via package manager or copying it over manually
|
||||||
|
ExecStart=/usr/local/bin/upda-server
|
||||||
|
```
|
||||||
|
|
||||||
|
For a full set of available configuration, look into the [Configuration](./Configuration.md) section. Furthermore,
|
||||||
|
it's recommended to set up proper [Monitoring](./Monitoring.md).
|
47
_doc/Home.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# upda
|
||||||
|
|
||||||
|
**Up**date **Da**shboard (upda). A simple application to keep track of updates from different hosts and systems.
|
||||||
|
|
||||||
|
Managing various application or OCI container image updates can be a tedious task:
|
||||||
|
|
||||||
|
* A lot of hosts to operate with a lot of different applications being deployed
|
||||||
|
* A lot of different OCI containers to watch for updated images
|
||||||
|
* No convenient dashboard to see and manage all the available updates in one place
|
||||||
|
|
||||||
|
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to be called
|
||||||
|
via a webhook call (created within _upda_) from other applications, such as a bash script, an
|
||||||
|
application like [duin](https://crazymax.dev/diun/) or simply by using the `upda-cli`.
|
||||||
|
|
||||||
|
Please head over to the [Usage](./Usage.md) section for a quick _Getting Started_ once you've [deployed](./Deployment.md)
|
||||||
|
_upda_.
|
||||||
|
|
||||||
|
The code is hosted here: [upda and CLI application including frontend](https://git.myservermanager.com/varakh/upda).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to get them
|
||||||
|
from an external source.
|
||||||
|
For this, _upda_ allows to manage webhooks, which can be called with a unique URL from any other application or even a
|
||||||
|
bash script so that upda retrieves these information.
|
||||||
|
|
||||||
|
_upda_'s main features include
|
||||||
|
|
||||||
|
* Managing [Updates](./Usage.md#manage-updates) by changing their state (pending, ignored, approved)
|
||||||
|
* Managing [Webhooks](./Usage.md#getting-updates-in-via-webhooks) which allow to get information into _upda_ regarding Updates
|
||||||
|
and their properties (like version) you like to track
|
||||||
|
* Managing [Actions](./Usage.md#actions) which allow you to further process changes made to an Update (created, state
|
||||||
|
changed, version
|
||||||
|
changed,), basically allowing you to invoke other systems with the help
|
||||||
|
of [shoutrrr](https://containrrr.dev/shoutrrr/)
|
||||||
|
* View [past invocation of Actions](./Usage.md#history-of-actions)
|
||||||
|
* Viewing [events](./Usage.md#see-what-has-changed) which allow you to see what has changed and how Updates
|
||||||
|
* [Metrics exporter](./Monitoring.md) via prometheus
|
||||||
|
|
||||||
|
_upda_ is designed to be simple. Only supported authorization mechanism is basic.
|
||||||
|
|
||||||
|
## What it is not
|
||||||
|
|
||||||
|
_upda_ is **NOT** a scraper to watch docker registries or GitHub releases, it simply tracks and consolidates updates
|
||||||
|
from different sources, but you need to feed in these information on your own, e.g., via Webhooks. If you like to watch
|
||||||
|
GitHub releases, write a scraper and use `upda-cli` to report back to _upda_.
|
||||||
|
|
174
_doc/Usage.md
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Getting started in _upda_ is easy after it has been [deployed](./Deployment.md) successfully and is reachable through your
|
||||||
|
browser.
|
||||||
|
|
||||||
|
![img](./img/updates.png)
|
||||||
|
|
||||||
|
## Login
|
||||||
|
|
||||||
|
Head over to the deployed _upda_ instance in your browser and login with your credentials.
|
||||||
|
|
||||||
|
![img](./img/login.png)
|
||||||
|
|
||||||
|
## Getting updates in via Webhooks
|
||||||
|
|
||||||
|
To get your first updates into _upda_, create a new Webhook. Webhooks are the central piece of how _upda_ gets notified
|
||||||
|
about updates.
|
||||||
|
|
||||||
|
![img](./img/webhooks.png)
|
||||||
|
|
||||||
|
After you've created a new Webhook, you should see
|
||||||
|
|
||||||
|
* a unique _upda_ `URL` which serves as entrypoint of other 3rd party applications,
|
||||||
|
e.g., `/api/v1/webhooks/<a unique identifier>` and
|
||||||
|
* a corresponding `token` (write it down somewhere, you won't see it again after initial creation) for the URL which
|
||||||
|
must be sent as `X-Webhook-Token` header when calling _upda_'s URL.
|
||||||
|
|
||||||
|
Next step is to make your 3rd party application use this webhook and bring in new updates into _upda_.
|
||||||
|
|
||||||
|
A good example is [duin](https://crazymax.dev/diun/), which is able to watch docker images for changes and updates. It
|
||||||
|
can be configured with a config file
|
||||||
|
and [diun's "notif" plugin supports calling external webhooks once a change is observed](https://crazymax.dev/diun/notif/webhook/).
|
||||||
|
We just need to configure _upda_ as the receiving application in diun's configuration file.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notif:
|
||||||
|
webhook:
|
||||||
|
endpoint: https://upda.domain.tld/api/v1/webhooks/ee03cd9e-04d0-4c7f-9866-efe219c2501e
|
||||||
|
method: POST
|
||||||
|
headers:
|
||||||
|
content-type: application/json
|
||||||
|
X-Webhook-Token: <the token from webhook creation in upda>
|
||||||
|
timeout: 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected payload is derived from the _type_ of the webhook which has been created in _upda_.
|
||||||
|
|
||||||
|
In addition, a webhook in _upda_ can be set to ignore the host. Please read more on that in the [Concepts](./Concepts.md)
|
||||||
|
section.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
Actions can be used to invoke arbitrary third party tools when an _event_ occurs, e.g., an update has been created or
|
||||||
|
modified. An action is triggered when its conditions are met, which means that the action's definition (event name,
|
||||||
|
host, application, provider) fits the change which happend in _upda_.
|
||||||
|
|
||||||
|
Actions have types. Different types require different payload to set them up. [shoutrrr](#shoutrrr) is supported as
|
||||||
|
action type, which can send notifications to a variety of services like Gotify, Ntfy, Teams, OpsGenie and many more.
|
||||||
|
It in turn also support invoking calls to an external URL. This means you can have a stream of events being triggered
|
||||||
|
when an update arrives in _upda_.
|
||||||
|
|
||||||
|
To create an Action, go to the Actions tab and click on _Create new action_. Enter the necessary information and
|
||||||
|
consult the Action's type documentation if necessary.
|
||||||
|
|
||||||
|
![img](./img/actions.png)
|
||||||
|
|
||||||
|
Supported events for Actions are the following:
|
||||||
|
|
||||||
|
| Event name | Description |
|
||||||
|
|:-------------------------|:--------------------------------------------------------------------|
|
||||||
|
| `update_created` | An update has been created |
|
||||||
|
| `update_updated` | An update has been updated (not necessarily its version attribute!) |
|
||||||
|
| `update_updated_state` | An update's state changed |
|
||||||
|
| `update_updated_version` | An update's version changed |
|
||||||
|
| `update_deleted` | An update has been removed |
|
||||||
|
|
||||||
|
For privacy, an action's configuration supports upda's **secrets** vault, which means that before an action is
|
||||||
|
triggered, any occurrence of `<SECRET>SECRET_KEY</SECRET>` is properly replaced by the value of the `SECRET_KEY` defined
|
||||||
|
inside the vault.
|
||||||
|
|
||||||
|
Secrets can be used in all payload for an Action, including shoutrrr's URL. To create a new secret, go to the _Secrets_
|
||||||
|
tab and click on _Create new secret_.
|
||||||
|
|
||||||
|
![img](./img/secrets.png)
|
||||||
|
|
||||||
|
In addition to secrets, upda provides **variables** which can be used with the `<VAR>VARIABLE_NAME</VAR>` syntax and any
|
||||||
|
occurrence is replaced before invocation as well.
|
||||||
|
|
||||||
|
| Variable name | Description |
|
||||||
|
|:-------------------------|:--------------------------------------------------|
|
||||||
|
| `<VAR>APPLICATION</VAR>` | The update's application name invoking the action |
|
||||||
|
| `<VAR>PROVIDER</VAR>` | The update's provider name invoking the action |
|
||||||
|
| `<VAR>HOST</VAR>` | The update's host invoking the action |
|
||||||
|
| `<VAR>VERSION</VAR>` | The update's version (latest) invoking the action |
|
||||||
|
| `<VAR>STATE</VAR>` | The update's state invoking the action |
|
||||||
|
|
||||||
|
#### shoutrrr
|
||||||
|
|
||||||
|
[shoutrrr](https://github.com/containrrr/shoutrrr?tab=readme-ov-file#documentation) supports multiple services directly
|
||||||
|
which can be provided as simple URL, e.g., `gotify://gotify.example.com:443/<token>`, where `<token>`
|
||||||
|
can also be provided as secret: `gotify://gotify.example.com:443/<SECRET>GOTIFY_TOKEN</SECRET>`.
|
||||||
|
|
||||||
|
##### shoutrrr: example for sending mails
|
||||||
|
|
||||||
|
Before starting, add the following _Secrets_:
|
||||||
|
|
||||||
|
```
|
||||||
|
MAIL_USER
|
||||||
|
MAIL_PASS
|
||||||
|
MAIL_HOST
|
||||||
|
MAIL_PORT
|
||||||
|
MAIL_FROM
|
||||||
|
MAIL_TO
|
||||||
|
```
|
||||||
|
|
||||||
|
For each event, now create a new _Action_ with different payload:
|
||||||
|
|
||||||
|
_New updates_
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
|:------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+New+Update` |
|
||||||
|
| Body | `New update '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) arrived on <VAR>HOST</VAR> for provider <VAR>PROVIDER</VAR>.` |
|
||||||
|
|
||||||
|
_Update changed_
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
|:------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+changed` |
|
||||||
|
| Body | `Update '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) changed on <VAR>HOST</VAR> for provider <VAR>PROVIDER</VAR>.` |
|
||||||
|
|
||||||
|
_Version changed_
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
|:------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+version+changed` |
|
||||||
|
| Body | `Update's version changed to '<VAR>VERSION</VAR>' for '<VAR>APPLICATION</VAR>' on <VAR>HOST</VAR> and provider <VAR>PROVIDER</VAR>.` |
|
||||||
|
|
||||||
|
_State changed_
|
||||||
|
|
||||||
|
| Field | Content |
|
||||||
|
|:------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+state+changed` |
|
||||||
|
| Body | `Update's state changed to '<VAR>STATE</VAR>' for '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) on <VAR>HOST</VAR> and provider <VAR>PROVIDER</VAR>.` |
|
||||||
|
|
||||||
|
In addition, you can have multiple URL fields, e.g., for sending a mail and a push notification.
|
||||||
|
|
||||||
|
### History of actions
|
||||||
|
|
||||||
|
Whenever new updates come in, are changed or an update's state changes, _upda_ enqueues all matching Actions.
|
||||||
|
|
||||||
|
If you head over to the Action History tab, you see pending, currently running, successful or error invocations of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
![img](./img/actions_history.png)
|
||||||
|
|
||||||
|
## Manage updates
|
||||||
|
|
||||||
|
Once Update are in _upda_, you can filter them by state, application or other properties to only see pending Updates for
|
||||||
|
example.
|
||||||
|
|
||||||
|
Furthermore, you can change their state to be ignored (see [Concepts](./Concepts.md)) or delete them.
|
||||||
|
|
||||||
|
![img](./img/updates.png)
|
||||||
|
|
||||||
|
In addition, you can view an Update's details by clicking on the small info icon for an Update.
|
||||||
|
|
||||||
|
![img](./img/updates_detail.png)
|
||||||
|
|
||||||
|
## See what has changed
|
||||||
|
|
||||||
|
For a full activity view, head over to the Events tab.
|
||||||
|
|
||||||
|
![img](./img/events.png)
|
1620
_doc/api.yaml
BIN
_doc/img/actions.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
_doc/img/actions_history.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
_doc/img/events.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
_doc/img/login.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
_doc/img/secrets.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
_doc/img/updates.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
_doc/img/updates_detail.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
_doc/img/webhooks.png
Normal file
After Width: | Height: | Size: 50 KiB |
|
@ -1,5 +1,15 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
|
const (
|
||||||
|
HeaderAppName = "X-App-Name"
|
||||||
|
HeaderAppVersion = "X-App-Version"
|
||||||
|
|
||||||
|
HeaderWebhookToken = "X-Webhook-Token"
|
||||||
|
|
||||||
|
HeaderContentType = "Content-Type"
|
||||||
|
HeaderContentTypeApplicationJson = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateState state of an update
|
// UpdateState state of an update
|
||||||
type UpdateState string
|
type UpdateState string
|
||||||
|
|
||||||
|
@ -41,14 +51,9 @@ type EventName string
|
||||||
const (
|
const (
|
||||||
EventNameUpdateCreated EventName = "update_created"
|
EventNameUpdateCreated EventName = "update_created"
|
||||||
EventNameUpdateUpdated EventName = "update_updated"
|
EventNameUpdateUpdated EventName = "update_updated"
|
||||||
EventNameUpdateUpdatedPending EventName = "update_updated_state_pending"
|
EventNameUpdateUpdatedState EventName = "update_updated_state"
|
||||||
EventNameUpdateUpdatedApproved EventName = "update_updated_state_approved"
|
EventNameUpdateUpdatedVersion EventName = "update_updated_version"
|
||||||
EventNameUpdateUpdatedIgnored EventName = "update_updated_state_ignored"
|
|
||||||
EventNameUpdateDeleted EventName = "update_deleted"
|
EventNameUpdateDeleted EventName = "update_deleted"
|
||||||
EventNameWebhookCreated EventName = "webhook_created"
|
|
||||||
EventNameWebhookUpdatedLabel EventName = "webhook_updated_label"
|
|
||||||
EventNameWebhookUpdatedIgnoreHost EventName = "webhook_updated_ignore_host"
|
|
||||||
EventNameWebhookDeleted EventName = "webhook_deleted"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *EventName) Scan(value interface{}) error {
|
func (e *EventName) Scan(value interface{}) error {
|
||||||
|
@ -65,6 +70,7 @@ type EventState string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventStateCreated EventState = "created"
|
EventStateCreated EventState = "created"
|
||||||
|
EventStateEnqueued EventState = "enqueued"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *EventState) Scan(value interface{}) error {
|
func (e *EventState) Scan(value interface{}) error {
|
||||||
|
@ -75,3 +81,39 @@ func (e *EventState) Scan(value interface{}) error {
|
||||||
func (e EventState) Value() string {
|
func (e EventState) Value() string {
|
||||||
return string(e)
|
return string(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActionType state of an update
|
||||||
|
type ActionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionTypeShoutrrr ActionType = "shoutrrr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *ActionType) Scan(value interface{}) error {
|
||||||
|
*e = ActionType(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ActionType) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionInvocationState state of an action invocation
|
||||||
|
type ActionInvocationState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionInvocationStateCreated ActionInvocationState = "created"
|
||||||
|
ActionInvocationStateRunning ActionInvocationState = "running"
|
||||||
|
ActionInvocationStateRetrying ActionInvocationState = "retrying"
|
||||||
|
ActionInvocationStateSuccess ActionInvocationState = "success"
|
||||||
|
ActionInvocationStateError ActionInvocationState = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *ActionInvocationState) Scan(value interface{}) error {
|
||||||
|
*e = ActionInvocationState(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ActionInvocationState) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
257
api/dto.go
|
@ -25,6 +25,63 @@ type CreateWebhookRequest struct {
|
||||||
IgnoreHost bool `json:"ignoreHost"`
|
IgnoreHost bool `json:"ignoreHost"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateSecretRequest struct {
|
||||||
|
Key string `json:"key" binding:"required,min=1"`
|
||||||
|
Value string `json:"value" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateActionRequest struct {
|
||||||
|
Label string `json:"label" binding:"required,min=1,max=255"`
|
||||||
|
Type string `json:"type" binding:"required,oneof=shoutrrr"`
|
||||||
|
MatchEvent *string `json:"matchEvent"`
|
||||||
|
MatchHost *string `json:"matchHost"`
|
||||||
|
MatchApplication *string `json:"matchApplication"`
|
||||||
|
MatchProvider *string `json:"matchProvider"`
|
||||||
|
Payload interface{} `json:"payload"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifySecretValueRequest struct {
|
||||||
|
Value string `json:"value" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionLabelRequest struct {
|
||||||
|
Label string `json:"label" binding:"required,min=1,max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionMatchEventRequest struct {
|
||||||
|
MatchEvent *string `json:"matchEvent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionMatchHostRequest struct {
|
||||||
|
MatchHost *string `json:"matchHost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionMatchApplicationRequest struct {
|
||||||
|
MatchApplication *string `json:"matchApplication"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionMatchProviderRequest struct {
|
||||||
|
MatchProvider *string `json:"matchProvider"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionTypeAndPayloadRequest struct {
|
||||||
|
Type ActionType `json:"type" binding:"required,oneof=shoutrrr"`
|
||||||
|
Payload interface{} `json:"payload" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyActionEnabledRequest struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestActionRequest struct {
|
||||||
|
Application string `json:"application" binding:"required,min=1"`
|
||||||
|
Provider string `json:"provider" binding:"required,min=1"`
|
||||||
|
Host string `json:"host" binding:"required,min=1"`
|
||||||
|
Version string `json:"version" binding:"required,min=1"`
|
||||||
|
State string `json:"state" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
type PaginateUpdateRequest struct {
|
type PaginateUpdateRequest struct {
|
||||||
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
||||||
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
||||||
|
@ -35,10 +92,24 @@ type PaginateUpdateRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginateWebhookRequest struct {
|
type PaginateWebhookRequest struct {
|
||||||
|
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
||||||
|
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
||||||
|
Order string `form:"order,default=asc" binding:"oneof=asc desc"`
|
||||||
|
OrderBy string `form:"orderBy,default=label" binding:"oneof=id label type created_at updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginateActionRequest struct {
|
||||||
|
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
||||||
|
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
||||||
|
Order string `form:"order,default=asc" binding:"oneof=asc desc"`
|
||||||
|
OrderBy string `form:"orderBy,default=label" binding:"oneof=id label type created_at updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginateActionInvocationRequest struct {
|
||||||
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
|
||||||
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
Page int `form:"page,default=1" binding:"numeric,gte=1"`
|
||||||
Order string `form:"order,default=desc" binding:"oneof=asc desc"`
|
Order string `form:"order,default=desc" binding:"oneof=asc desc"`
|
||||||
OrderBy string `form:"orderBy,default=updated_at" binding:"oneof=id label type created_at updated_at"`
|
OrderBy string `form:"orderBy,default=created_at" binding:"oneof=id state retry_count created_at updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebhookGenericRequest struct {
|
type WebhookGenericRequest struct {
|
||||||
|
@ -216,12 +287,25 @@ func NewWebhookPageResponse(content []*WebhookResponse, page int, pageSize int,
|
||||||
type EventResponse struct {
|
type EventResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
State string `json:"state"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
Payload interface{} `json:"payload,omitempty"`
|
Payload interface{} `json:"payload,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventSingleResponse struct {
|
||||||
|
Data EventResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventSingleResponse(id uuid.UUID, name string, createdAt time.Time, updatedAt time.Time, payload interface{}) *EventSingleResponse {
|
||||||
|
e := new(EventSingleResponse)
|
||||||
|
e.Data.ID = id
|
||||||
|
e.Data.Name = name
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
e.Data.Payload = payload
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
type EventWindowResponse struct {
|
type EventWindowResponse struct {
|
||||||
Content []*EventResponse `json:"content"`
|
Content []*EventResponse `json:"content"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
|
@ -256,6 +340,7 @@ type EventPayloadUpdateDeletedDto struct {
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEventWindowResponse(content []*EventResponse, size int, skip int, orderBy string, order string, hasNext bool) *EventWindowResponse {
|
func NewEventWindowResponse(content []*EventResponse, size int, skip int, orderBy string, order string, hasNext bool) *EventWindowResponse {
|
||||||
|
@ -269,24 +354,160 @@ func NewEventWindowResponse(content []*EventResponse, size int, skip int, orderB
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventPayloadWebhookCreatedDto struct {
|
type SecretResponse struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
ID uuid.UUID `json:"id"`
|
||||||
Label string `json:"label,omitempty"`
|
Key string `json:"key"`
|
||||||
Type string `json:"type,omitempty"`
|
Value string `json:"value,omitempty"`
|
||||||
IgnoreHost bool `json:"ignoreHost"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventPayloadWebhookUpdatedDto struct {
|
type SecretSingleResponse struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
Data SecretResponse `json:"data"`
|
||||||
LabelPrior string `json:"labelPrior,omitempty"`
|
|
||||||
Label string `json:"label,omitempty"`
|
|
||||||
IgnoreHostPrior bool `json:"ignoreHostPrior"`
|
|
||||||
IgnoreHost bool `json:"ignoreHost"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventPayloadWebhookDeletedDto struct {
|
func NewSecretSingleResponse(id uuid.UUID, key string, value string, createdAt time.Time, updatedAt time.Time) *SecretSingleResponse {
|
||||||
Label string `json:"label,omitempty"`
|
e := new(SecretSingleResponse)
|
||||||
Type string `json:"type,omitempty"`
|
e.Data.ID = id
|
||||||
IgnoreHost bool `json:"ignoreHost"`
|
e.Data.Key = key
|
||||||
|
e.Data.Value = value
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretPageResponse struct {
|
||||||
|
Content []*SecretResponse `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecretDataPageResponse struct {
|
||||||
|
Data *SecretPageResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecretPageResponse(content []*SecretResponse) *SecretPageResponse {
|
||||||
|
e := new(SecretPageResponse)
|
||||||
|
e.Content = content
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
MatchEvent *string `json:"matchEvent,omitempty"`
|
||||||
|
MatchHost *string `json:"matchHost,omitempty"`
|
||||||
|
MatchApplication *string `json:"matchApplication,omitempty"`
|
||||||
|
MatchProvider *string `json:"matchProvider,omitempty"`
|
||||||
|
Payload interface{} `json:"payload,omitempty"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionSingleResponse struct {
|
||||||
|
Data ActionResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionSingleResponse(id uuid.UUID, label string, t string, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}, enabled bool, createdAt time.Time, updatedAt time.Time) *ActionSingleResponse {
|
||||||
|
e := new(ActionSingleResponse)
|
||||||
|
e.Data.ID = id
|
||||||
|
e.Data.Label = label
|
||||||
|
e.Data.Type = t
|
||||||
|
e.Data.MatchEvent = matchEvent
|
||||||
|
e.Data.MatchHost = matchHost
|
||||||
|
e.Data.MatchApplication = matchApplication
|
||||||
|
e.Data.MatchProvider = matchProvider
|
||||||
|
e.Data.Payload = payload
|
||||||
|
e.Data.Enabled = enabled
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionPageResponse struct {
|
||||||
|
Content []*ActionResponse `json:"content"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
OrderBy string `json:"orderBy"`
|
||||||
|
Order string `json:"order"`
|
||||||
|
TotalElements int64 `json:"totalElements"`
|
||||||
|
TotalPages int64 `json:"totalPages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionPageResponse(content []*ActionResponse, page int, pageSize int, orderBy string, order string, totalElements int64, totalPages int64) *ActionPageResponse {
|
||||||
|
e := new(ActionPageResponse)
|
||||||
|
e.Content = content
|
||||||
|
e.Page = page
|
||||||
|
e.PageSize = pageSize
|
||||||
|
e.OrderBy = orderBy
|
||||||
|
e.Order = order
|
||||||
|
e.TotalElements = totalElements
|
||||||
|
e.TotalPages = totalPages
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionTestResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionTestSingleResponse struct {
|
||||||
|
Data ActionTestResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionTestSingleResponse(success bool, message string) *ActionTestSingleResponse {
|
||||||
|
e := new(ActionTestSingleResponse)
|
||||||
|
e.Data.Success = success
|
||||||
|
e.Data.Message = message
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionInvocationResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
RetryCount int `json:"retryCount"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Message *string `json:"message,omitempty"`
|
||||||
|
ActionID string `json:"actionId"`
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionInvocationSingleResponse struct {
|
||||||
|
Data ActionInvocationResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionInvocationSingleResponse(id uuid.UUID, retryCount int, state string, message *string, actionId string, eventId string, createdAt time.Time, updatedAt time.Time) *ActionInvocationSingleResponse {
|
||||||
|
e := new(ActionInvocationSingleResponse)
|
||||||
|
e.Data.ID = id
|
||||||
|
e.Data.RetryCount = retryCount
|
||||||
|
e.Data.State = state
|
||||||
|
e.Data.Message = message
|
||||||
|
e.Data.ActionID = actionId
|
||||||
|
e.Data.EventID = eventId
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionInvocationPageResponse struct {
|
||||||
|
Content []*ActionInvocationResponse `json:"content"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
OrderBy string `json:"orderBy"`
|
||||||
|
Order string `json:"order"`
|
||||||
|
TotalElements int64 `json:"totalElements"`
|
||||||
|
TotalPages int64 `json:"totalPages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionInvocationPageResponse(content []*ActionInvocationResponse, page int, pageSize int, orderBy string, order string, totalElements int64, totalPages int64) *ActionInvocationPageResponse {
|
||||||
|
e := new(ActionInvocationPageResponse)
|
||||||
|
e.Content = content
|
||||||
|
e.Page = page
|
||||||
|
e.PageSize = pageSize
|
||||||
|
e.OrderBy = orderBy
|
||||||
|
e.Order = order
|
||||||
|
e.TotalElements = totalElements
|
||||||
|
e.TotalPages = totalPages
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
5
commons/constants.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package commons
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version = "4.0.1"
|
||||||
|
)
|
102
go.mod
|
@ -1,77 +1,87 @@
|
||||||
module git.myservermanager.com/varakh/upda
|
module git.myservermanager.com/varakh/upda
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
|
toolchain go1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Depado/ginprom v1.8.0
|
github.com/Depado/ginprom v1.8.1
|
||||||
github.com/adrg/xdg v0.4.0
|
github.com/adrg/xdg v0.5.1
|
||||||
github.com/gin-contrib/cors v1.5.0
|
github.com/containrrr/shoutrrr v0.8.0
|
||||||
github.com/gin-contrib/zap v0.2.0
|
github.com/gin-contrib/cors v1.7.2
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-contrib/static v1.1.2
|
||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/gin-contrib/zap v1.1.4
|
||||||
github.com/go-co-op/gocron-redis-lock v1.3.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/go-playground/validator/v10 v10.17.0
|
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1
|
||||||
github.com/go-resty/resty/v2 v2.11.0
|
github.com/go-co-op/gocron/v2 v2.12.1
|
||||||
github.com/google/uuid v1.5.0
|
github.com/go-playground/validator/v10 v10.22.1
|
||||||
github.com/redis/go-redis/v9 v9.4.0
|
github.com/go-redsync/redsync/v4 v4.13.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/go-resty/resty/v2 v2.15.3
|
||||||
github.com/urfave/cli/v2 v2.27.1
|
github.com/google/uuid v1.6.0
|
||||||
go.uber.org/zap v1.26.0
|
github.com/redis/go-redis/v9 v9.7.0
|
||||||
gorm.io/driver/postgres v1.5.4
|
github.com/stretchr/testify v1.9.0
|
||||||
gorm.io/driver/sqlite v1.5.4
|
github.com/urfave/cli/v2 v2.27.5
|
||||||
gorm.io/gorm v1.25.5
|
go.uber.org/automaxprocs v1.6.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
gorm.io/driver/postgres v1.5.9
|
||||||
|
gorm.io/driver/sqlite v1.5.6
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
moul.io/zapgorm2 v1.3.0
|
moul.io/zapgorm2 v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.10.1 // indirect
|
github.com/bytedance/sonic v1.12.1 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/fatih/color v1.15.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-redsync/redsync/v4 v4.11.0 // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.11.1 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.5.0 // indirect
|
golang.org/x/arch v0.9.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
299
go.sum
|
@ -2,14 +2,14 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Depado/ginprom v1.8.0 h1:zaaibRLNI1dMiiuj1MKzatm8qrcHzikMlCc1anqOdyo=
|
github.com/Depado/ginprom v1.8.1 h1:lrQTddbRqlHq1j6SpJDySDumJlR7FEybzdX0PS3HXPc=
|
||||||
github.com/Depado/ginprom v1.8.0/go.mod h1:XBaKzeNBqPF4vxJpNLincSQZeMDnZp1tIbU0FU0UKgg=
|
github.com/Depado/ginprom v1.8.1/go.mod h1:9Z+ahPJLSeMndDfnDTfiuBn2SKVAuL2yvihApWzof9A=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA=
|
github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA=
|
||||||
github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg=
|
github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg=
|
||||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
github.com/adrg/xdg v0.5.1 h1:Im8iDbEFARltY09yOJlSGu4Asjk2vF85+3Dyru8uJ0U=
|
||||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
github.com/adrg/xdg v0.5.1/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
||||||
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
@ -19,29 +19,29 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
|
||||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
|
||||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
|
||||||
github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4=
|
github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4=
|
||||||
github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8=
|
github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||||
|
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||||
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
|
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
|
||||||
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -55,20 +55,26 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||||
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
|
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||||
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
|
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||||
|
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||||
|
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-contrib/zap v0.2.0 h1:HLvt3rZXyC8XC+s2lHzMFow3UDqiEbfrBWJyHHS6L8A=
|
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
|
||||||
github.com/gin-contrib/zap v0.2.0/go.mod h1:eqfbe9ZmI+GgTZF6nRiC2ZwDeM4DK1Viwc8OxTCphh0=
|
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-contrib/zap v1.1.4 h1:xvxTybg6XBdNtcQLH3Tf0lFr4vhDkwzgLLrIGlNTqIo=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-contrib/zap v1.1.4/go.mod h1:7lgEpe91kLbeJkwBTPgtVBy4zMa6oSBEcvj662diqKQ=
|
||||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-co-op/gocron-redis-lock v1.3.0 h1:PKwtuc/BhrDll/DxJfnXoW/+D1VXubd47xcGaB9pDuM=
|
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1 h1:xM+mzO88L+kODvY4vIUVLlZuyWazK5vJfK0DiFachdQ=
|
||||||
github.com/go-co-op/gocron-redis-lock v1.3.0/go.mod h1:9+H7ZfqVtJfx94uEAELwH+uHkn1UpM6lRM99wOBTGtg=
|
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1/go.mod h1:FSHZ13f4bfH37RpJi9l3vl2GTiJRUI6xTDbUvXLoqrY=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||||
|
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||||
|
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
@ -77,39 +83,35 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
|
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||||
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
|
||||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
|
||||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
|
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||||
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-redsync/redsync/v4 v4.11.0 h1:OPEcAxHBb95EzfwCKWM93ksOwHd5bTce2BD4+R14N6k=
|
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
|
||||||
github.com/go-redsync/redsync/v4 v4.11.0/go.mod h1:ZfayzutkgeBmEmBlUR3j+rF6kN44UUGtEdfzhBFZTPc=
|
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
|
||||||
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
|
||||||
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||||
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
||||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
@ -119,42 +121,49 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||||
|
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||||
|
@ -168,15 +177,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||||
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||||
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
|
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
|
||||||
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
|
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
@ -184,24 +196,22 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||||
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||||
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||||
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
|
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
@ -215,14 +225,15 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
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=
|
||||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
||||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
||||||
github.com/testcontainers/testcontainers-go v0.26.0 h1:uqcYdoOHBy1ca7gKODfBd9uTHVK3a7UL848z09MVZ0c=
|
github.com/testcontainers/testcontainers-go v0.26.0 h1:uqcYdoOHBy1ca7gKODfBd9uTHVK3a7UL848z09MVZ0c=
|
||||||
|
@ -235,132 +246,98 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||||
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
|
||||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
|
||||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||||
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||||
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
|
||||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||||
google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
|
google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
|
||||||
google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||||
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
|
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
|
||||||
moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs=
|
moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
|
|
@ -8,8 +8,15 @@
|
||||||
"group:recommended"
|
"group:recommended"
|
||||||
],
|
],
|
||||||
"prConcurrentLimit": 0,
|
"prConcurrentLimit": 0,
|
||||||
|
"schedule": [
|
||||||
|
"monthly"
|
||||||
|
],
|
||||||
|
// security
|
||||||
|
"osvVulnerabilityAlerts": true,
|
||||||
|
"dependencyDashboardOSVVulnerabilitySummary": "all",
|
||||||
// skip next alpine, see https://github.com/mattn/go-sqlite3/issues/1164
|
// skip next alpine, see https://github.com/mattn/go-sqlite3/issues/1164
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
|
// oci
|
||||||
{
|
{
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"alpine"
|
"alpine"
|
||||||
|
@ -20,25 +27,70 @@
|
||||||
],
|
],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
|
// go
|
||||||
{
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"gomod"
|
||||||
|
],
|
||||||
"matchPackagePrefixes": [
|
"matchPackagePrefixes": [
|
||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron"
|
||||||
],
|
],
|
||||||
"groupName": "gocron"
|
"groupName": "gocron"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"gomod"
|
||||||
|
],
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": [
|
||||||
"minor"
|
"minor"
|
||||||
],
|
],
|
||||||
"groupName": "all minor dependencies",
|
"groupName": "GoLang: all minor dependencies",
|
||||||
"groupSlug": "all-minor-deps"
|
"groupSlug": "golang-all-minor-deps"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"gomod"
|
||||||
|
],
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": [
|
||||||
"patch"
|
"patch"
|
||||||
],
|
],
|
||||||
"groupName": "all patch dependencies",
|
"groupName": "GoLang: all patch dependencies",
|
||||||
"groupSlug": "all-patch-deps"
|
"groupSlug": "golang-all-patch-deps"
|
||||||
|
},
|
||||||
|
// node
|
||||||
|
// GLOBAL: ignore @types/node major and minor (manual upgrade with pipeline required)
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"@types/node"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"major",
|
||||||
|
"minor"
|
||||||
|
],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor"
|
||||||
|
],
|
||||||
|
"groupName": "Node: all minor dependencies",
|
||||||
|
"groupSlug": "node-all-minor-deps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"groupName": "Node: all patch dependencies",
|
||||||
|
"groupSlug": "node-all-patch-deps"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
230
server/api_handler_action.go
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionHandler struct {
|
||||||
|
service actionService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionHandler(s *actionService) *actionHandler {
|
||||||
|
return &actionHandler{service: *s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) paginate(c *gin.Context) {
|
||||||
|
var queryParams api.PaginateActionRequest
|
||||||
|
var err error
|
||||||
|
if err = c.ShouldBindQuery(&queryParams); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions []*Action
|
||||||
|
if actions, err = h.service.paginate(queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.ActionResponse
|
||||||
|
data = make([]*api.ActionResponse, 0, len(actions))
|
||||||
|
|
||||||
|
for _, e := range actions {
|
||||||
|
data = append(data, &api.ActionResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Label: e.Label,
|
||||||
|
Type: e.Type,
|
||||||
|
MatchEvent: e.MatchEvent,
|
||||||
|
MatchHost: e.MatchHost,
|
||||||
|
MatchApplication: e.MatchApplication,
|
||||||
|
MatchProvider: e.MatchProvider,
|
||||||
|
Payload: e.Payload,
|
||||||
|
Enabled: e.Enabled,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalElements int64
|
||||||
|
if totalElements, err = h.service.count(); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (totalElements + int64(queryParams.PageSize) - 1) / int64(queryParams.PageSize)
|
||||||
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewActionPageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) get(c *gin.Context) {
|
||||||
|
e, err := h.service.get(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) create(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.CreateActionRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.create(req.Label, api.ActionType(req.Type), req.MatchEvent, req.MatchHost, req.MatchApplication, req.MatchProvider, req.Payload, req.Enabled); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateLabel(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionLabelRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateLabel(c.Param("id"), req.Label); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateMatchEvent(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionMatchEventRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateMatchEvent(c.Param("id"), req.MatchEvent); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateMatchHost(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionMatchHostRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateMatchHost(c.Param("id"), req.MatchHost); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateMatchApplication(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionMatchApplicationRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateMatchApplication(c.Param("id"), req.MatchApplication); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateMatchProvider(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionMatchProviderRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateMatchProvider(c.Param("id"), req.MatchProvider); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updatePayload(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionTypeAndPayloadRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateTypeAndPayload(c.Param("id"), req.Type, req.Payload); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) updateEnabled(c *gin.Context) {
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyActionEnabledRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateEnabled(c.Param("id"), req.Enabled); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionSingleResponse(e.ID, e.Label, e.Type, e.MatchEvent, e.MatchHost, e.MatchApplication, e.MatchProvider, e.Payload, e.Enabled, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionHandler) delete(c *gin.Context) {
|
||||||
|
if err := h.service.delete(c.Param("id")); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
102
server/api_handler_action_invocation.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionInvocationHandler struct {
|
||||||
|
actionService actionService
|
||||||
|
actionInvocationService actionInvocationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionInvocationHandler(as *actionService, ais *actionInvocationService) *actionInvocationHandler {
|
||||||
|
return &actionInvocationHandler{actionService: *as, actionInvocationService: *ais}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionInvocationHandler) test(c *gin.Context) {
|
||||||
|
var err error
|
||||||
|
var req api.TestActionRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
if e, err = h.actionService.get(c.Param("id")); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.actionInvocationService.execute(e, &eventPayloadInformationDto{Application: req.Application, Host: req.Host, Provider: req.Provider, Version: req.Version, State: req.State})
|
||||||
|
|
||||||
|
isSuccess := err == nil
|
||||||
|
var message string
|
||||||
|
if err != nil {
|
||||||
|
message = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionTestSingleResponse(isSuccess, message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionInvocationHandler) paginate(c *gin.Context) {
|
||||||
|
var queryParams api.PaginateActionInvocationRequest
|
||||||
|
var err error
|
||||||
|
if err = c.ShouldBindQuery(&queryParams); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionInvocations []*ActionInvocation
|
||||||
|
if actionInvocations, err = h.actionInvocationService.paginate(queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.ActionInvocationResponse
|
||||||
|
data = make([]*api.ActionInvocationResponse, 0, len(actionInvocations))
|
||||||
|
|
||||||
|
for _, e := range actionInvocations {
|
||||||
|
data = append(data, &api.ActionInvocationResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
RetryCount: e.RetryCount,
|
||||||
|
State: e.State,
|
||||||
|
Message: e.Message,
|
||||||
|
ActionID: e.ActionID,
|
||||||
|
EventID: e.EventID,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalElements int64
|
||||||
|
if totalElements, err = h.actionInvocationService.count(); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (totalElements + int64(queryParams.PageSize) - 1) / int64(queryParams.PageSize)
|
||||||
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewActionInvocationPageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionInvocationHandler) get(c *gin.Context) {
|
||||||
|
e, err := h.actionInvocationService.get(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewActionInvocationSingleResponse(e.ID, e.RetryCount, e.State, e.Message, e.ActionID, e.EventID, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *actionInvocationHandler) delete(c *gin.Context) {
|
||||||
|
if err := h.actionInvocationService.delete(c.Param("id")); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -13,6 +14,6 @@ func newAuthHandler() *authHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *authHandler) login(c *gin.Context) {
|
func (h *authHandler) login(c *gin.Context) {
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
"git.myservermanager.com/varakh/upda/util"
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
@ -19,8 +20,8 @@ func errAbortWithValidatorPayload(c *gin.Context, err error) {
|
||||||
errorMap[key] = txt
|
errorMap[key] = txt
|
||||||
}
|
}
|
||||||
|
|
||||||
resErr := newServiceError(IllegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
resErr := newServiceError(illegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -29,17 +30,19 @@ func errToHttpStatus(err error) int {
|
||||||
var e *serviceError
|
var e *serviceError
|
||||||
switch {
|
switch {
|
||||||
case errors.As(err, &e):
|
case errors.As(err, &e):
|
||||||
if e.Status == IllegalArgument {
|
if e.Status == illegalArgument {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
} else if e.Status == Unauthorized {
|
} else if e.Status == unauthorized {
|
||||||
return http.StatusUnauthorized
|
return http.StatusUnauthorized
|
||||||
} else if e.Status == Forbidden {
|
} else if e.Status == forbidden {
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
} else if e.Status == NotFound {
|
} else if e.Status == notFound {
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
} else if e.Status == Conflict {
|
} else if e.Status == methodNotAllowed {
|
||||||
|
return http.StatusMethodNotAllowed
|
||||||
|
} else if e.Status == conflict {
|
||||||
return http.StatusConflict
|
return http.StatusConflict
|
||||||
} else if e.Status == General {
|
} else if e.Status == general {
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -57,7 +60,7 @@ func errCodeToStr(err error) string {
|
||||||
return string(e.Status)
|
return string(e.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(General)
|
return string(general)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
||||||
|
|
|
@ -29,13 +29,12 @@ func (h *eventHandler) window(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []*api.EventResponse
|
var data []*api.EventResponse
|
||||||
data = make([]*api.EventResponse, 0)
|
data = make([]*api.EventResponse, 0, len(events))
|
||||||
|
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
data = append(data, &api.EventResponse{
|
data = append(data, &api.EventResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
State: e.State,
|
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
Payload: e.Payload,
|
Payload: e.Payload,
|
||||||
|
@ -51,12 +50,22 @@ func (h *eventHandler) window(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewEventWindowResponse(data, queryParams.Size, queryParams.Skip, queryParams.OrderBy, queryParams.Order, hasNext)))
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewEventWindowResponse(data, queryParams.Size, queryParams.Skip, queryParams.OrderBy, queryParams.Order, hasNext)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *eventHandler) get(c *gin.Context) {
|
||||||
|
e, err := h.service.get(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewEventSingleResponse(e.ID, e.Name, e.CreatedAt, e.UpdatedAt, e.Payload))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *eventHandler) delete(c *gin.Context) {
|
func (h *eventHandler) delete(c *gin.Context) {
|
||||||
if err := h.service.delete(c.Param("id")); err != nil {
|
if err := h.service.delete(c.Param("id")); err != nil {
|
||||||
_ = c.AbortWithError(errToHttpStatus(err), err)
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ func newHealthHandler() *healthHandler {
|
||||||
return &healthHandler{}
|
return &healthHandler{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healthHandler) showHealth(c *gin.Context) {
|
func (h *healthHandler) show(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||||
"healthy": true,
|
"healthy": true,
|
||||||
}})
|
}})
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/commons"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -14,10 +15,10 @@ func newInfoHandler(a *appConfig) *infoHandler {
|
||||||
return &infoHandler{appConfig: *a}
|
return &infoHandler{appConfig: *a}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *infoHandler) showInfo(c *gin.Context) {
|
func (h *infoHandler) show(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||||
"name": Name,
|
"name": name,
|
||||||
"version": Version,
|
"Version": commons.Version,
|
||||||
"timeZone": h.appConfig.timeZone,
|
"timeZone": h.appConfig.timeZone,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
97
server/api_handler_secret.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type secretHandler struct {
|
||||||
|
service secretService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecretHandler(s *secretService) *secretHandler {
|
||||||
|
return &secretHandler{service: *s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *secretHandler) getAll(c *gin.Context) {
|
||||||
|
var secrets []*Secret
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if secrets, err = h.service.getAll(); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.SecretResponse
|
||||||
|
data = make([]*api.SecretResponse, 0, len(secrets))
|
||||||
|
|
||||||
|
for _, e := range secrets {
|
||||||
|
data = append(data, &api.SecretResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Key: e.Key,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewSecretPageResponse(data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *secretHandler) create(c *gin.Context) {
|
||||||
|
var e *Secret
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.CreateSecretRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.upsert(req.Key, req.Value); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewSecretSingleResponse(e.ID, e.Key, e.Value, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *secretHandler) updateValue(c *gin.Context) {
|
||||||
|
var e *Secret
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifySecretValueRequest
|
||||||
|
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateValue(c.Param("id"), req.Value); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewSecretSingleResponse(e.ID, e.Key, e.Value, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *secretHandler) get(c *gin.Context) {
|
||||||
|
e, err := h.service.get(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewSecretSingleResponse(e.ID, e.Key, "", e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *secretHandler) delete(c *gin.Context) {
|
||||||
|
if err := h.service.delete(c.Param("id")); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ func (h *updateHandler) paginate(c *gin.Context) {
|
||||||
|
|
||||||
s, stateQueryContainsAtLeastOne := c.GetQueryArray("state")
|
s, stateQueryContainsAtLeastOne := c.GetQueryArray("state")
|
||||||
|
|
||||||
var states []api.UpdateState
|
states := make([]api.UpdateState, 0)
|
||||||
if stateQueryContainsAtLeastOne {
|
if stateQueryContainsAtLeastOne {
|
||||||
for _, state := range s {
|
for _, state := range s {
|
||||||
states = append(states, api.UpdateState(state))
|
states = append(states, api.UpdateState(state))
|
||||||
|
@ -82,7 +82,7 @@ func (h *updateHandler) updateState(c *gin.Context) {
|
||||||
|
|
||||||
var req api.ModifyUpdateStateRequest
|
var req api.ModifyUpdateStateRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
errAbortWithValidatorPayload(c, err)
|
errAbortWithValidatorPayload(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -101,6 +101,6 @@ func (h *updateHandler) delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (h *webhookHandler) paginate(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []*api.WebhookResponse
|
var data []*api.WebhookResponse
|
||||||
data = make([]*api.WebhookResponse, 0)
|
data = make([]*api.WebhookResponse, 0, len(webhooks))
|
||||||
|
|
||||||
for _, e := range webhooks {
|
for _, e := range webhooks {
|
||||||
data = append(data, &api.WebhookResponse{
|
data = append(data, &api.WebhookResponse{
|
||||||
|
@ -52,13 +52,23 @@ func (h *webhookHandler) paginate(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewWebhookPageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewWebhookPageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *webhookHandler) get(c *gin.Context) {
|
||||||
|
e, err := h.service.get(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewWebhookSingleResponse(e.ID, e.Label, e.Type, e.IgnoreHost, "", e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *webhookHandler) create(c *gin.Context) {
|
func (h *webhookHandler) create(c *gin.Context) {
|
||||||
var e *Webhook
|
var e *Webhook
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var req api.CreateWebhookRequest
|
var req api.CreateWebhookRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
errAbortWithValidatorPayload(c, err)
|
errAbortWithValidatorPayload(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -77,7 +87,7 @@ func (h *webhookHandler) updateLabel(c *gin.Context) {
|
||||||
|
|
||||||
var req api.ModifyWebhookLabelRequest
|
var req api.ModifyWebhookLabelRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
errAbortWithValidatorPayload(c, err)
|
errAbortWithValidatorPayload(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -96,7 +106,7 @@ func (h *webhookHandler) updateIgnoreHost(c *gin.Context) {
|
||||||
|
|
||||||
var req api.ModifyWebhookIgnoreHostRequest
|
var req api.ModifyWebhookIgnoreHostRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
errAbortWithValidatorPayload(c, err)
|
errAbortWithValidatorPayload(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -115,6 +125,6 @@ func (h *webhookHandler) delete(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ func newWebhookInvocationHandler(i *webhookInvocationService, w *webhookService)
|
||||||
return &webhookInvocationHandler{invocationService: *i, webhookService: *w}
|
return &webhookInvocationHandler{invocationService: *i, webhookService: *w}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *webhookInvocationHandler) executeWebhookGeneric(c *gin.Context) {
|
func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
||||||
tokenHeader := c.GetHeader(HeaderWebhookToken)
|
tokenHeader := c.GetHeader(api.HeaderWebhookToken)
|
||||||
webhookId := c.Param("id")
|
webhookId := c.Param("id")
|
||||||
|
|
||||||
var w *Webhook
|
var w *Webhook
|
||||||
|
@ -52,11 +52,11 @@ func (h *webhookInvocationHandler) executeWebhookGeneric(c *gin.Context) {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
err = newServiceError(IllegalArgument, errors.New("no default handler for webhook type found"))
|
err = newServiceError(illegalArgument, errors.New("no default handler for webhook type found"))
|
||||||
_ = c.AbortWithError(errToHttpStatus(err), err)
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,27 +3,46 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/commons"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func middlewareAppName() gin.HandlerFunc {
|
func middlewareAppName() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header(HeaderAppName, Name)
|
c.Header(api.HeaderAppName, name)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareGlobalNotFound() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, api.NewErrorResponseWithStatusAndMessage(string(notFound), "page not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareGlobalMethodNotAllowed() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, api.NewErrorResponseWithStatusAndMessage(string(methodNotAllowed), "method not allowed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareEnforceJsonContentType() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.Method != http.MethodOptions && !strings.HasPrefix(c.GetHeader(api.HeaderContentType), api.HeaderContentTypeApplicationJson) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, api.NewErrorResponseWithStatusAndMessage(string(illegalArgument), "content-type must be application/json"))
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func middlewareAppVersion() gin.HandlerFunc {
|
func middlewareAppVersion() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header(HeaderAppVersion, Version)
|
c.Header(api.HeaderAppVersion, commons.Version)
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func middlewareAppContentType() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +55,7 @@ func middlewareErrorHandler() gin.HandlerFunc {
|
||||||
|
|
||||||
if len(c.Errors) > 0 {
|
if len(c.Errors) > 0 {
|
||||||
// status -1 doesn't overwrite existing status code
|
// status -1 doesn't overwrite existing status code
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||||
c.JSON(-1, api.NewErrorResponseWithStatusAndMessage(errCodeToStr(c.Errors.Last()), c.Errors.Last().Error()))
|
c.JSON(-1, api.NewErrorResponseWithStatusAndMessage(errCodeToStr(c.Errors.Last()), c.Errors.Last().Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -47,7 +66,7 @@ func middlewareAppErrorRecoveryHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(General), fmt.Sprintf("%s", err)))
|
c.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(general), fmt.Sprintf("%s", err)))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
202
server/app.go
|
@ -6,8 +6,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.myservermanager.com/varakh/upda/util"
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
|
ginstatic "github.com/gin-contrib/static"
|
||||||
ginzap "github.com/gin-contrib/zap"
|
ginzap "github.com/gin-contrib/zap"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
_ "go.uber.org/automaxprocs"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -23,88 +25,186 @@ func Start() {
|
||||||
// secure init
|
// secure init
|
||||||
util.AssertAvailablePRNG()
|
util.AssertAvailablePRNG()
|
||||||
|
|
||||||
// set gin mode derived from logging level
|
// set gin mode derived
|
||||||
if zap.L().Level() == zap.DebugLevel {
|
if env.appConfig.isDevelopment {
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.SetMode(gin.DebugMode)
|
||||||
} else {
|
} else {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// app init (router, services, handlers)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(ginzap.Ginzap(zap.L(), time.RFC3339, false))
|
router.Use(ginzap.Ginzap(zap.L(), time.RFC3339, false))
|
||||||
router.Use(ginzap.RecoveryWithZap(zap.L(), true))
|
router.Use(ginzap.RecoveryWithZap(zap.L(), true))
|
||||||
|
|
||||||
// metrics
|
var err error
|
||||||
prometheusService := newPrometheusService(router, env.prometheusConfig)
|
|
||||||
|
ps := newPrometheusService(router, env.prometheusConfig)
|
||||||
|
|
||||||
if env.prometheusConfig.enabled {
|
if env.prometheusConfig.enabled {
|
||||||
prometheusService.init()
|
if err = ps.init(); err != nil {
|
||||||
router.Use(prometheusService.prometheus.Instrument())
|
zap.L().Sugar().Fatalf("Prometheus service init failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
router.Use(ps.prometheus.Instrument())
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRepo := newUpdateDbRepo(env.db)
|
updateRepo := newUpdateDbRepo(env.db)
|
||||||
webhookRepo := newWebhookDbRepo(env.db)
|
webhookRepo := newWebhookDbRepo(env.db)
|
||||||
eventRepo := newEventDbRepo(env.db)
|
eventRepo := newEventDbRepo(env.db)
|
||||||
|
secretRepo := newSecretDbRepo(env.db)
|
||||||
|
actionRepo := newActionDbRepo(env.db)
|
||||||
|
actionInvocationRepo := newActionInvocationDbRepo(env.db)
|
||||||
|
|
||||||
lockService := newLockMemService()
|
var ls lockService
|
||||||
|
|
||||||
eventService := newEventService(eventRepo)
|
if env.lockConfig.redisEnabled {
|
||||||
updateService := newUpdateService(updateRepo, eventService, prometheusService)
|
var e error
|
||||||
webhookService := newWebhookService(webhookRepo, env.webhookConfig, eventService)
|
ls, e = newLockRedisService(env.lockConfig)
|
||||||
webhookInvocationService := newWebhookInvocationService(webhookService, updateService, env.webhookConfig)
|
|
||||||
|
|
||||||
taskService := newTaskService(updateService, eventService, webhookService, lockService, prometheusService, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig)
|
if err != nil {
|
||||||
taskService.init()
|
zap.L().Fatal("Failed to create lock service", zap.Error(e))
|
||||||
taskService.start()
|
}
|
||||||
|
} else {
|
||||||
|
ls = newLockMemService()
|
||||||
|
}
|
||||||
|
|
||||||
updateHandler := newUpdateHandler(updateService, env.appConfig)
|
es := newEventService(eventRepo)
|
||||||
webhookHandler := newWebhookHandler(webhookService)
|
us := newUpdateService(updateRepo, es)
|
||||||
webhookInvocationHandler := newWebhookInvocationHandler(webhookInvocationService, webhookService)
|
ws := newWebhookService(webhookRepo, env.webhookConfig)
|
||||||
eventHandler := newEventHandler(eventService)
|
wis := newWebhookInvocationService(ws, us, env.webhookConfig)
|
||||||
infoHandler := newInfoHandler(env.appConfig)
|
|
||||||
healthHandler := newHealthHandler()
|
ss := newSecretService(secretRepo)
|
||||||
authHandler := newAuthHandler()
|
as := newActionService(actionRepo, es)
|
||||||
|
ais := newActionInvocationService(actionInvocationRepo, as, es, ss)
|
||||||
|
|
||||||
|
var ts *taskService
|
||||||
|
|
||||||
|
if ts, err = newTaskService(us, es, ws, as, ais, ls, ps, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Task service creation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ts.init(); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Task service initialization failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.start()
|
||||||
|
|
||||||
|
uh := newUpdateHandler(us, env.appConfig)
|
||||||
|
wh := newWebhookHandler(ws)
|
||||||
|
wih := newWebhookInvocationHandler(wis, ws)
|
||||||
|
eh := newEventHandler(es)
|
||||||
|
sh := newSecretHandler(ss)
|
||||||
|
ah := newActionHandler(as)
|
||||||
|
aih := newActionInvocationHandler(as, ais)
|
||||||
|
|
||||||
|
ih := newInfoHandler(env.appConfig)
|
||||||
|
hh := newHealthHandler()
|
||||||
|
authH := newAuthHandler()
|
||||||
|
|
||||||
router.Use(middlewareAppName())
|
router.Use(middlewareAppName())
|
||||||
router.Use(middlewareAppVersion())
|
router.Use(middlewareAppVersion())
|
||||||
router.Use(middlewareAppContentType())
|
|
||||||
router.Use(middlewareErrorHandler())
|
router.Use(middlewareErrorHandler())
|
||||||
router.Use(middlewareAppErrorRecoveryHandler())
|
router.Use(middlewareAppErrorRecoveryHandler())
|
||||||
|
router.NoRoute(middlewareGlobalNotFound())
|
||||||
|
router.NoMethod(middlewareGlobalMethodNotAllowed())
|
||||||
|
|
||||||
|
// in production mode, the frontend is embedded on / during compile time utilizing -tags prod
|
||||||
|
// if the prod tag is missing, development setup is used and a dummy frontend is shown on /
|
||||||
|
var targetPath string
|
||||||
|
if env.appConfig.isDevelopment {
|
||||||
|
targetPath = "web_dev"
|
||||||
|
} else {
|
||||||
|
targetPath = "web/build"
|
||||||
|
}
|
||||||
|
router.Use(ginstatic.Serve("/", ginstatic.EmbedFolder(embeddedFiles, targetPath)))
|
||||||
|
|
||||||
|
if !env.appConfig.isDevelopment {
|
||||||
|
embeddedFrontendGroup := router.Group("/")
|
||||||
|
embeddedFrontendGroup.GET("/conf/runtime-config.js", func(c *gin.Context) {
|
||||||
|
config := `
|
||||||
|
const runtime_config = Object.freeze({
|
||||||
|
VITE_API_URL: '%s/api/v1/',
|
||||||
|
VITE_APP_TITLE: '%s'
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'runtime_config', {
|
||||||
|
value: runtime_config,
|
||||||
|
writable: false
|
||||||
|
});
|
||||||
|
`
|
||||||
|
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(fmt.Sprintf(config, env.webConfig.apiUrl, env.webConfig.title)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
router.Use(cors.New(cors.Config{
|
router.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: env.serverConfig.corsAllowOrigin,
|
AllowOrigins: env.serverConfig.corsAllowOrigins,
|
||||||
AllowMethods: env.serverConfig.corsAllowMethods,
|
AllowMethods: env.serverConfig.corsAllowMethods,
|
||||||
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
||||||
AllowCredentials: true,
|
AllowCredentials: env.serverConfig.corsAllowCredentials,
|
||||||
|
ExposeHeaders: env.serverConfig.corsExposeHeaders,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiPublicGroup := router.Group("/api/v1")
|
apiPublicGroup := router.Group("/api/v1")
|
||||||
apiPublicGroup.GET("/health", healthHandler.showHealth)
|
apiPublicGroup.GET("/health", hh.show)
|
||||||
apiPublicGroup.GET("/info", infoHandler.showInfo)
|
apiPublicGroup.GET("/info", ih.show)
|
||||||
|
|
||||||
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.executeWebhookGeneric)
|
apiPublicGroup.POST("/webhooks/:id", middlewareEnforceJsonContentType(), wih.execute)
|
||||||
|
|
||||||
apiAuthGroup := router.Group("/api/v1", gin.BasicAuth(gin.Accounts{
|
var authMethodHandler gin.HandlerFunc
|
||||||
env.authConfig.adminUser: env.authConfig.adminPassword,
|
|
||||||
}))
|
|
||||||
|
|
||||||
apiAuthGroup.GET("/login", authHandler.login)
|
if authModeBasicSingle == env.authConfig.authMethod {
|
||||||
|
authMethodHandler = gin.BasicAuth(gin.Accounts{
|
||||||
|
env.authConfig.basicAuthUser: env.authConfig.basicAuthPassword,
|
||||||
|
})
|
||||||
|
} else if authModeBasicCredentials == env.authConfig.authMethod {
|
||||||
|
authMethodHandler = gin.BasicAuth(env.authConfig.basicAuthCredentials)
|
||||||
|
} else {
|
||||||
|
zap.L().Fatal("No valid auth mode found")
|
||||||
|
}
|
||||||
|
|
||||||
apiAuthGroup.GET("/updates", updateHandler.paginate)
|
apiAuthGroup := router.Group("/api/v1", authMethodHandler)
|
||||||
apiAuthGroup.GET("/updates/:id", updateHandler.get)
|
|
||||||
apiAuthGroup.PATCH("/updates/:id/state", updateHandler.updateState)
|
|
||||||
apiAuthGroup.DELETE("/updates/:id", updateHandler.delete)
|
|
||||||
|
|
||||||
apiAuthGroup.GET("/webhooks", webhookHandler.paginate)
|
apiAuthGroup.GET("/login", authH.login)
|
||||||
apiAuthGroup.POST("/webhooks", webhookHandler.create)
|
|
||||||
apiAuthGroup.PATCH("/webhooks/:id/label", webhookHandler.updateLabel)
|
|
||||||
apiAuthGroup.PATCH("/webhooks/:id/ignore-host", webhookHandler.updateIgnoreHost)
|
|
||||||
apiAuthGroup.DELETE("/webhooks/:id", webhookHandler.delete)
|
|
||||||
|
|
||||||
apiAuthGroup.GET("/events", eventHandler.window)
|
apiAuthGroup.GET("/updates", uh.paginate)
|
||||||
apiAuthGroup.DELETE("/events/:id", eventHandler.delete)
|
apiAuthGroup.GET("/updates/:id", uh.get)
|
||||||
|
apiAuthGroup.PATCH("/updates/:id/state", middlewareEnforceJsonContentType(), uh.updateState)
|
||||||
|
apiAuthGroup.DELETE("/updates/:id", uh.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/webhooks", wh.paginate)
|
||||||
|
apiAuthGroup.POST("/webhooks", middlewareEnforceJsonContentType(), wh.create)
|
||||||
|
apiAuthGroup.GET("/webhooks/:id", wh.get)
|
||||||
|
apiAuthGroup.PATCH("/webhooks/:id/label", middlewareEnforceJsonContentType(), wh.updateLabel)
|
||||||
|
apiAuthGroup.PATCH("/webhooks/:id/ignore-host", middlewareEnforceJsonContentType(), wh.updateIgnoreHost)
|
||||||
|
apiAuthGroup.DELETE("/webhooks/:id", wh.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/events", eh.window)
|
||||||
|
apiAuthGroup.GET("/events/:id", eh.get)
|
||||||
|
apiAuthGroup.DELETE("/events/:id", eh.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/secrets", sh.getAll)
|
||||||
|
apiAuthGroup.GET("/secrets/:id", sh.get)
|
||||||
|
apiAuthGroup.POST("/secrets", middlewareEnforceJsonContentType(), sh.create)
|
||||||
|
apiAuthGroup.PATCH("/secrets/:id/value", middlewareEnforceJsonContentType(), sh.updateValue)
|
||||||
|
apiAuthGroup.DELETE("/secrets/:id", sh.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/actions", ah.paginate)
|
||||||
|
apiAuthGroup.POST("/actions", middlewareEnforceJsonContentType(), ah.create)
|
||||||
|
apiAuthGroup.GET("/actions/:id", ah.get)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/label", middlewareEnforceJsonContentType(), ah.updateLabel)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/match-event", middlewareEnforceJsonContentType(), ah.updateMatchEvent)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/match-host", middlewareEnforceJsonContentType(), ah.updateMatchHost)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/match-application", middlewareEnforceJsonContentType(), ah.updateMatchApplication)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/match-provider", middlewareEnforceJsonContentType(), ah.updateMatchProvider)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/payload", middlewareEnforceJsonContentType(), ah.updatePayload)
|
||||||
|
apiAuthGroup.PATCH("/actions/:id/enabled", middlewareEnforceJsonContentType(), ah.updateEnabled)
|
||||||
|
apiAuthGroup.DELETE("/actions/:id", ah.delete)
|
||||||
|
apiAuthGroup.POST("/actions/:id/test", middlewareEnforceJsonContentType(), aih.test)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/action-invocations", aih.paginate)
|
||||||
|
apiAuthGroup.GET("/action-invocations/:id", aih.get)
|
||||||
|
apiAuthGroup.DELETE("/action-invocations/:id", aih.delete)
|
||||||
|
|
||||||
// start server
|
|
||||||
serverAddress := fmt.Sprintf("%s:%d", env.serverConfig.listen, env.serverConfig.port)
|
serverAddress := fmt.Sprintf("%s:%d", env.serverConfig.listen, env.serverConfig.port)
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: serverAddress,
|
Addr: serverAddress,
|
||||||
|
@ -112,34 +212,34 @@ func Start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var e error
|
||||||
|
|
||||||
if env.serverConfig.tlsEnabled {
|
if env.serverConfig.tlsEnabled {
|
||||||
err = srv.ListenAndServeTLS(env.serverConfig.tlsCertPath, env.serverConfig.tlsKeyPath)
|
e = srv.ListenAndServeTLS(env.serverConfig.tlsCertPath, env.serverConfig.tlsKeyPath)
|
||||||
} else {
|
} else {
|
||||||
err = srv.ListenAndServe()
|
e = srv.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if e != nil && !errors.Is(e, http.ErrServerClosed) {
|
||||||
zap.L().Sugar().Fatalf("Application cannot be started: %v", err)
|
zap.L().Sugar().Fatalf("Application cannot be started: %v", e)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// gracefully handle shut down
|
// gracefully handle shut down
|
||||||
// Wait for interrupt signal to gracefully shut down the server with
|
// Wait for interrupt signal to gracefully shut down the server with
|
||||||
// a timeout of x seconds.
|
// a timeout of x seconds.
|
||||||
quit := make(chan os.Signal)
|
quit := make(chan os.Signal, 1)
|
||||||
// kill (no param) default send syscall.SIGTERM
|
// kill (no param) default send syscall.SIGTERM
|
||||||
// kill -2 is syscall.SIGINT
|
// kill -2 is syscall.SIGINT
|
||||||
// kill -9 is syscall. SIGKILL but cannot be caught, thus no need to add
|
// kill -9 is syscall. SIGKILL but cannot be caught, thus no need to add
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
zap.L().Info("Shutting down...")
|
zap.L().Info("Shutting down...")
|
||||||
taskService.stop()
|
ts.stop()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), env.serverConfig.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), env.serverConfig.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
if err = srv.Shutdown(ctx); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Shutdown failed, exited directly: %v", err)
|
zap.L().Sugar().Fatalf("Shutdown failed, exited directly: %v", err)
|
||||||
}
|
}
|
||||||
// catching ctx.Done() for configured timeout
|
// catching ctx.Done() for configured timeout
|
||||||
|
|
9
server/app_embedded_ui_dev.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !prod
|
||||||
|
// +build !prod
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed web_dev
|
||||||
|
var embeddedFiles embed.FS
|
9
server/app_embedded_ui_prod.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build prod
|
||||||
|
// +build prod
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed web/build/*
|
||||||
|
var embeddedFiles embed.FS
|
|
@ -1,11 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
const (
|
|
||||||
HeaderAppName = "X-App-Name"
|
|
||||||
HeaderAppVersion = "X-App-Version"
|
|
||||||
|
|
||||||
HeaderWebhookToken = "X-Webhook-Token"
|
|
||||||
|
|
||||||
headerContentType = "Content-Type"
|
|
||||||
headerContentTypeApplicationJson = "application/json"
|
|
||||||
)
|
|
|
@ -1,6 +1,5 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "upda"
|
name = "upda"
|
||||||
Version = "1.0.3"
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,34 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
envDevelopment = "DEVELOPMENT"
|
||||||
|
|
||||||
|
envLoggingLevel = "LOGGING_LEVEL"
|
||||||
|
loggingLevelDefault = "info"
|
||||||
|
envLoggingEncoding = "LOGGING_ENCODING"
|
||||||
|
loggingEncodingDefault = "json"
|
||||||
|
|
||||||
|
envLoggingDirectory = "LOGGING_DIRECTORY"
|
||||||
|
loggingFileNameDefault = "upda.log"
|
||||||
|
|
||||||
|
envSecret = "SECRET"
|
||||||
|
|
||||||
envTZ = "TZ"
|
envTZ = "TZ"
|
||||||
tzDefault = "Europe/Berlin"
|
tzDefault = "Europe/Berlin"
|
||||||
|
|
||||||
envAdminUser = "ADMIN_USER"
|
envWebApiUrl = "WEB_API_URL"
|
||||||
envAdminPassword = "ADMIN_PASSWORD"
|
webApiUrlDefault = "http://localhost"
|
||||||
|
|
||||||
envLoggingLevel = "LOGGING_LEVEL"
|
envWebTitle = "WEB_TITLE"
|
||||||
|
webTitleDefault = "upda"
|
||||||
|
|
||||||
|
envAuthMode = "AUTH_MODE"
|
||||||
|
authModeDefault = authModeBasicSingle
|
||||||
|
authModeBasicSingle = "basic_single"
|
||||||
|
authModeBasicCredentials = "basic_credentials"
|
||||||
|
envBasicAuthUser = "BASIC_AUTH_USER"
|
||||||
|
envBasicAuthPassword = "BASIC_AUTH_PASSWORD"
|
||||||
|
envBasicAuthCredentials = "BASIC_AUTH_CREDENTIALS"
|
||||||
|
|
||||||
envServerPort = "SERVER_PORT"
|
envServerPort = "SERVER_PORT"
|
||||||
envServerListen = "SERVER_LISTEN"
|
envServerListen = "SERVER_LISTEN"
|
||||||
|
@ -20,12 +41,16 @@ const (
|
||||||
serverTlsEnabledDefault = "false"
|
serverTlsEnabledDefault = "false"
|
||||||
serverTimeoutDefault = "1s"
|
serverTimeoutDefault = "1s"
|
||||||
|
|
||||||
envCorsAllowOrigin = "CORS_ALLOW_ORIGIN"
|
envCorsAllowOrigins = "CORS_ALLOW_ORIGINS"
|
||||||
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
||||||
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
||||||
corsAllowOriginDefault = "*"
|
envCorsAllowCredentials = "CORS_ALLOW_CREDENTIALS"
|
||||||
|
envCorsExposeHeaders = "CORS_EXPOSE_HEADERS"
|
||||||
|
corsAllowOriginsDefault = "*"
|
||||||
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||||
corsAllowHeadersDefault = "Authorization, Content-Type"
|
corsAllowHeadersDefault = "Authorization, Content-Type"
|
||||||
|
corsAllowCredentialsDefault = "true"
|
||||||
|
corsExposeHeadersDefault = "*"
|
||||||
|
|
||||||
dbTypeSqlite = "sqlite"
|
dbTypeSqlite = "sqlite"
|
||||||
dbTypePostgres = "postgres"
|
dbTypePostgres = "postgres"
|
||||||
|
@ -62,7 +87,7 @@ const (
|
||||||
envTaskUpdateCleanStaleMaxAge = "TASK_UPDATE_CLEAN_STALE_MAX_AGE"
|
envTaskUpdateCleanStaleMaxAge = "TASK_UPDATE_CLEAN_STALE_MAX_AGE"
|
||||||
taskUpdateCleanStaleEnabledDefault = "false"
|
taskUpdateCleanStaleEnabledDefault = "false"
|
||||||
taskUpdateCleanStaleIntervalDefault = "1h"
|
taskUpdateCleanStaleIntervalDefault = "1h"
|
||||||
taskUpdateCleanStaleMaxAgeDefault = "168h"
|
taskUpdateCleanStaleMaxAgeDefault = "720h"
|
||||||
|
|
||||||
envTaskEventCleanStaleEnabled = "TASK_EVENT_CLEAN_STALE_ENABLED"
|
envTaskEventCleanStaleEnabled = "TASK_EVENT_CLEAN_STALE_ENABLED"
|
||||||
envTaskEventCleanStaleInterval = "TASK_EVENT_CLEAN_STALE_INTERVAL"
|
envTaskEventCleanStaleInterval = "TASK_EVENT_CLEAN_STALE_INTERVAL"
|
||||||
|
@ -71,6 +96,29 @@ const (
|
||||||
taskEventCleanStaleIntervalDefault = "8h"
|
taskEventCleanStaleIntervalDefault = "8h"
|
||||||
taskEventCleanStaleMaxAgeDefault = "2190h"
|
taskEventCleanStaleMaxAgeDefault = "2190h"
|
||||||
|
|
||||||
|
envTaskActionsEnqueueEnabled = "TASK_ACTIONS_ENQUEUE_ENABLED"
|
||||||
|
envTaskActionsEnqueueInterval = "TASK_ACTIONS_ENQUEUE_INTERVAL"
|
||||||
|
envTaskActionsEnqueueBatchSize = "TASK_ACTIONS_ENQUEUE_BATCH_SIZE"
|
||||||
|
taskActionsEnqueueEnabledDefault = "true"
|
||||||
|
taskActionsEnqueueIntervalDefault = "10s"
|
||||||
|
taskActionsEnqueueBatchSizeDefault = "1"
|
||||||
|
|
||||||
|
envTaskActionsInvokeEnabled = "TASK_ACTIONS_INVOKE_ENABLED"
|
||||||
|
envTaskActionsInvokeInterval = "TASK_ACTIONS_INVOKE_INTERVAL"
|
||||||
|
envTaskActionsInvokeBatchSize = "TASK_ACTIONS_INVOKE_BATCH_SIZE"
|
||||||
|
envTaskActionsInvokeMaxRetries = "TASK_ACTIONS_INVOKE_MAX_RETRIES"
|
||||||
|
taskActionsInvokeEnabledDefault = "true"
|
||||||
|
taskActionsInvokeIntervalDefault = "10s"
|
||||||
|
taskActionsInvokeBatchSizeDefault = "1"
|
||||||
|
taskActionsInvokeMaxRetriesDefault = "3"
|
||||||
|
|
||||||
|
envTaskActionsCleanStaleEnabled = "TASK_ACTIONS_CLEAN_STALE_ENABLED"
|
||||||
|
envTaskActionsCleanStaleInterval = "TASK_ACTIONS_CLEAN_STALE_INTERVAL"
|
||||||
|
envTaskActionsCleanStaleMaxAge = "TASK_ACTIONS_CLEAN_STALE_MAX_AGE"
|
||||||
|
taskActionsCleanStaleEnabledDefault = "true"
|
||||||
|
taskActionsCleanStaleIntervalDefault = "12h"
|
||||||
|
taskActionsCleanStaleMaxAgeDefault = "720h"
|
||||||
|
|
||||||
envLockRedisEnabled = "LOCK_REDIS_ENABLED"
|
envLockRedisEnabled = "LOCK_REDIS_ENABLED"
|
||||||
envLockRedisUrl = "LOCK_REDIS_URL"
|
envLockRedisUrl = "LOCK_REDIS_URL"
|
||||||
redisEnabledDefault = "false"
|
redisEnabledDefault = "false"
|
||||||
|
|
|
@ -13,12 +13,12 @@ const (
|
||||||
metricUpdatesApproved = "updates_approved"
|
metricUpdatesApproved = "updates_approved"
|
||||||
metricUpdatesApprovedHelp = "amount of all updates in approved state"
|
metricUpdatesApprovedHelp = "amount of all updates in approved state"
|
||||||
|
|
||||||
metricUpdates = "updates"
|
|
||||||
metricUpdatesHelp = "details for all updates, 0=pending, 1=approved, 2=ignored"
|
|
||||||
|
|
||||||
metricWebhooks = "webhooks"
|
metricWebhooks = "webhooks"
|
||||||
metricWebhooksHelp = "amount of all webhooks"
|
metricWebhooksHelp = "amount of all webhooks"
|
||||||
|
|
||||||
metricEvents = "events"
|
metricEvents = "events"
|
||||||
metricEventsHelp = "amount of all events"
|
metricEventsHelp = "amount of all events"
|
||||||
|
|
||||||
|
metricActions = "actions"
|
||||||
|
metricActionsHelp = "amount of all actions"
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSONMap defined JSON data type, need to implements driver.Valuer, sql.Scanner interface
|
// JSONMap defined JSON data type, need to implement driver.Valuer, sql.Scanner interface
|
||||||
type JSONMap map[string]interface {
|
type JSONMap map[string]interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ func (m JSONMap) Value() (driver.Value, error) {
|
||||||
return string(ba), err
|
return string(ba), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan scan value into Jsonb, implements sql.Scanner interface
|
// Scan value into JSONB, implements sql.Scanner interface
|
||||||
func (m *JSONMap) Scan(val interface{}) error {
|
func (m *JSONMap) Scan(val interface{}) error {
|
||||||
if val == nil {
|
if val == nil {
|
||||||
*m = make(JSONMap)
|
*m = make(JSONMap)
|
||||||
|
|
16
server/dto.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
// DTOs
|
||||||
|
|
||||||
|
type actionPayloadShoutrrrDto struct {
|
||||||
|
Body string `json:"body" binding:"required" validate:"required"`
|
||||||
|
Urls []string `json:"urls" binding:"required" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventPayloadInformationDto struct {
|
||||||
|
Host string
|
||||||
|
Application string
|
||||||
|
Provider string
|
||||||
|
Version string
|
||||||
|
State string
|
||||||
|
}
|
167
server/entity.go
|
@ -1,8 +1,10 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,16 +13,6 @@ func (u *Update) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wh *Webhook) BeforeCreate(tx *gorm.DB) (err error) {
|
|
||||||
wh.ID = uuid.New()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Event) BeforeCreate(tx *gorm.DB) (err error) {
|
|
||||||
e.ID = uuid.New()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update entity holding information for updates
|
// Update entity holding information for updates
|
||||||
type Update struct {
|
type Update struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
@ -34,6 +26,57 @@ type Update struct {
|
||||||
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate encrypts webhook token before storing to database
|
||||||
|
func (wh *Webhook) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var encryptedToken string
|
||||||
|
|
||||||
|
if encryptedToken, er = util.EncryptAndEncode(wh.Token, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.ID = uuid.New()
|
||||||
|
wh.Token = encryptedToken
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate encrypts webhook token before storing to database
|
||||||
|
func (wh *Webhook) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var encryptedValue string
|
||||||
|
|
||||||
|
if encryptedValue, er = util.EncryptAndEncode(wh.Token, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.Token = encryptedValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterSave decrypt webhook token after encrypted value has been retrieved from database
|
||||||
|
func (wh *Webhook) AfterSave(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var decrypted string
|
||||||
|
if decrypted, er = util.DecryptAndDecode(wh.Token, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.Token = decrypted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterFind decrypt webhook token after encrypted value has been retrieved from database
|
||||||
|
func (wh *Webhook) AfterFind(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var decrypted string
|
||||||
|
if decrypted, er = util.DecryptAndDecode(wh.Token, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.Token = decrypted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Webhook entity holding information for webhooks
|
// Webhook entity holding information for webhooks
|
||||||
type Webhook struct {
|
type Webhook struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
@ -45,6 +88,11 @@ type Webhook struct {
|
||||||
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Event) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
e.ID = uuid.New()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Event entity holding information for events
|
// Event entity holding information for events
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
@ -54,3 +102,102 @@ type Event struct {
|
||||||
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BeforeCreate encrypts secret value before storing to database
|
||||||
|
func (e *Secret) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var encryptedValue string
|
||||||
|
|
||||||
|
if encryptedValue, er = util.EncryptAndEncode(e.Value, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
e.ID = uuid.New()
|
||||||
|
e.Value = encryptedValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate encrypts secret value before storing to database
|
||||||
|
func (e *Secret) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var encryptedValue string
|
||||||
|
|
||||||
|
if encryptedValue, er = util.EncryptAndEncode(e.Value, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Value = encryptedValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterSave decrypt secret value after encrypted value has been retrieved from database
|
||||||
|
func (e *Secret) AfterSave(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var decrypted string
|
||||||
|
if decrypted, er = util.DecryptAndDecode(e.Value, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Value = decrypted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterFind decrypt secret value after encrypted value has been retrieved from database
|
||||||
|
func (e *Secret) AfterFind(tx *gorm.DB) (err error) {
|
||||||
|
var er error
|
||||||
|
var decrypted string
|
||||||
|
if decrypted, er = util.DecryptAndDecode(e.Value, os.Getenv(envSecret)); er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Value = decrypted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret entity holding information for secrets
|
||||||
|
type Secret struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
Key string `gorm:"unique;not null"`
|
||||||
|
Value string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Action) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
e.ID = uuid.New()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action entity holding information for actions
|
||||||
|
type Action struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
Label string `gorm:"not null"`
|
||||||
|
Type string `gorm:"not null"`
|
||||||
|
MatchEvent *string `gorm:""`
|
||||||
|
MatchApplication *string `gorm:""`
|
||||||
|
MatchProvider *string `gorm:""`
|
||||||
|
MatchHost *string `gorm:""`
|
||||||
|
Payload JSONMap `gorm:"jsonb"`
|
||||||
|
Enabled bool `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ActionInvocation) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
e.ID = uuid.New()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionInvocation entity holding information for invocations of actions
|
||||||
|
type ActionInvocation struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
RetryCount int `gorm:"not null;default:1"`
|
||||||
|
State string `gorm:"not null"`
|
||||||
|
Message *string
|
||||||
|
Event Event `gorm:"foreignKey:EventID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
|
EventID string `gorm:"not null"`
|
||||||
|
Action Action `gorm:"foreignKey:ActionID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
|
ActionID string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,34 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
"log"
|
"log"
|
||||||
"moul.io/zapgorm2"
|
"moul.io/zapgorm2"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type appConfig struct {
|
type appConfig struct {
|
||||||
timeZone string
|
timeZone string
|
||||||
|
isDevelopment bool
|
||||||
|
isDebug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type webConfig struct {
|
||||||
|
title string
|
||||||
|
apiUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
|
@ -26,24 +38,38 @@ type serverConfig struct {
|
||||||
tlsCertPath string
|
tlsCertPath string
|
||||||
tlsKeyPath string
|
tlsKeyPath string
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
corsAllowOrigin []string
|
corsAllowCredentials bool
|
||||||
|
corsAllowOrigins []string
|
||||||
corsAllowMethods []string
|
corsAllowMethods []string
|
||||||
corsAllowHeaders []string
|
corsAllowHeaders []string
|
||||||
|
corsExposeHeaders []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type authConfig struct {
|
type authConfig struct {
|
||||||
adminUser string
|
authMethod string
|
||||||
adminPassword string
|
basicAuthUser string
|
||||||
|
basicAuthPassword string
|
||||||
|
basicAuthCredentials map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type taskConfig struct {
|
type taskConfig struct {
|
||||||
updateCleanStaleEnabled bool
|
updateCleanStaleEnabled bool
|
||||||
updateCleanStaleInterval string
|
updateCleanStaleInterval time.Duration
|
||||||
updateCleanStaleMaxAge time.Duration
|
updateCleanStaleMaxAge time.Duration
|
||||||
eventCleanStaleEnabled bool
|
eventCleanStaleEnabled bool
|
||||||
eventCleanStaleInterval string
|
eventCleanStaleInterval time.Duration
|
||||||
eventCleanStaleMaxAge time.Duration
|
eventCleanStaleMaxAge time.Duration
|
||||||
prometheusRefreshInterval string
|
actionsEnqueueEnabled bool
|
||||||
|
actionsEnqueueInterval time.Duration
|
||||||
|
actionsEnqueueBatchSize int
|
||||||
|
actionsInvokeEnabled bool
|
||||||
|
actionsInvokeInterval time.Duration
|
||||||
|
actionsInvokeBatchSize int
|
||||||
|
actionsInvokeMaxRetries int
|
||||||
|
actionsCleanStaleEnabled bool
|
||||||
|
actionsCleanStaleInterval time.Duration
|
||||||
|
actionsCleanStaleMaxAge time.Duration
|
||||||
|
prometheusRefreshInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type lockConfig struct {
|
type lockConfig struct {
|
||||||
|
@ -64,6 +90,7 @@ type prometheusConfig struct {
|
||||||
|
|
||||||
type Environment struct {
|
type Environment struct {
|
||||||
appConfig *appConfig
|
appConfig *appConfig
|
||||||
|
webConfig *webConfig
|
||||||
authConfig *authConfig
|
authConfig *authConfig
|
||||||
serverConfig *serverConfig
|
serverConfig *serverConfig
|
||||||
taskConfig *taskConfig
|
taskConfig *taskConfig
|
||||||
|
@ -74,34 +101,99 @@ type Environment struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapEnvironment() *Environment {
|
func bootstrapEnvironment() *Environment {
|
||||||
// logging (configured independently)
|
|
||||||
var logger *zap.Logger
|
|
||||||
var err error
|
var err error
|
||||||
level := zap.NewAtomicLevelAt(zapcore.InfoLevel)
|
|
||||||
|
|
||||||
envLoggingLevel := os.Getenv(envLoggingLevel)
|
// bootstrap logging (configured independently and required before any other action)
|
||||||
if envLoggingLevel != "" {
|
loggingLevel := os.Getenv(envLoggingLevel)
|
||||||
if level, err = zap.ParseAtomicLevel(envLoggingLevel); err != nil {
|
if loggingLevel == "" {
|
||||||
|
if err = os.Setenv(envLoggingLevel, loggingLevelDefault); err != nil {
|
||||||
|
log.Fatalf("Cannot set logging level: %v", err)
|
||||||
|
}
|
||||||
|
loggingLevel = os.Getenv(envLoggingLevel)
|
||||||
|
}
|
||||||
|
var level zap.AtomicLevel
|
||||||
|
if level, err = zap.ParseAtomicLevel(loggingLevel); err != nil {
|
||||||
log.Fatalf("Cannot parse logging level: %v", err)
|
log.Fatalf("Cannot parse logging level: %v", err)
|
||||||
}
|
}
|
||||||
|
loggingEncoding := os.Getenv(envLoggingEncoding)
|
||||||
|
if loggingEncoding == "" {
|
||||||
|
if err = os.Setenv(envLoggingEncoding, loggingEncodingDefault); err != nil {
|
||||||
|
log.Fatalf("Cannot set logging encoding: %v", err)
|
||||||
|
}
|
||||||
|
loggingEncoding = os.Getenv(envLoggingEncoding)
|
||||||
|
}
|
||||||
|
if loggingEncoding != "json" && loggingEncoding != "console" {
|
||||||
|
log.Fatalf("Cannot parse logging level: %v", errors.New("only 'json' and 'console' are allowed logging encodings"))
|
||||||
|
}
|
||||||
|
isDebug := level.Level() == zap.DebugLevel
|
||||||
|
isDevelopment := os.Getenv(envDevelopment) == "true"
|
||||||
|
var loggingEncoderConfig zapcore.EncoderConfig
|
||||||
|
if loggingEncoding == "json" {
|
||||||
|
loggingEncoderConfig = zap.NewProductionEncoderConfig()
|
||||||
|
} else {
|
||||||
|
loggingEncoderConfig = zap.NewDevelopmentEncoderConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger, err = zap.NewDevelopment(zap.IncreaseLevel(level))
|
logPaths := []string{"stderr"}
|
||||||
if err != nil {
|
loggingDirectory := os.Getenv(envLoggingDirectory)
|
||||||
log.Fatalf("Can't initialize logger: %v", err)
|
|
||||||
}
|
|
||||||
// flushes buffer, if any
|
|
||||||
defer logger.Sync()
|
|
||||||
|
|
||||||
zap.ReplaceGlobals(logger)
|
if loggingDirectory != "" {
|
||||||
|
logFile := filepath.Join(loggingDirectory, loggingFileNameDefault)
|
||||||
|
|
||||||
|
if err = util.CreateFileWithParent(logFile); err != nil {
|
||||||
|
log.Fatalf("Log file '%s' cannot be created: %v", loggingDirectory, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logPaths = append(logPaths, logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zapConfig *zap.Config
|
||||||
|
if isDebug {
|
||||||
|
zapConfig = &zap.Config{
|
||||||
|
Level: level,
|
||||||
|
Development: isDevelopment,
|
||||||
|
Encoding: loggingEncoding,
|
||||||
|
EncoderConfig: loggingEncoderConfig,
|
||||||
|
OutputPaths: logPaths,
|
||||||
|
ErrorOutputPaths: logPaths,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zapConfig = &zap.Config{
|
||||||
|
Level: level,
|
||||||
|
Development: isDevelopment,
|
||||||
|
Sampling: &zap.SamplingConfig{
|
||||||
|
Initial: 100,
|
||||||
|
Thereafter: 100,
|
||||||
|
},
|
||||||
|
Encoding: loggingEncoding,
|
||||||
|
EncoderConfig: loggingEncoderConfig,
|
||||||
|
OutputPaths: logPaths,
|
||||||
|
ErrorOutputPaths: logPaths,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zapLogger := zap.Must(zapConfig.Build())
|
||||||
|
defer func(zapLogger *zap.Logger) {
|
||||||
|
_ = zapLogger.Sync()
|
||||||
|
}(zapLogger)
|
||||||
|
zap.ReplaceGlobals(zapLogger)
|
||||||
|
|
||||||
// assign defaults from given environment variables and validate
|
// assign defaults from given environment variables and validate
|
||||||
bootstrapFromEnvironmentAndValidate()
|
bootstrapFromEnvironmentAndValidate()
|
||||||
|
|
||||||
// parse environment variables in actual configuration structs
|
// parse environment variables in actual configuration structs
|
||||||
// app config
|
// app config
|
||||||
appConfig := &appConfig{
|
ac := &appConfig{
|
||||||
timeZone: os.Getenv(envTZ),
|
timeZone: os.Getenv(envTZ),
|
||||||
|
isDebug: isDebug,
|
||||||
|
isDevelopment: isDevelopment,
|
||||||
|
}
|
||||||
|
|
||||||
|
// web config
|
||||||
|
var webC *webConfig
|
||||||
|
webC = &webConfig{
|
||||||
|
title: os.Getenv(envWebTitle),
|
||||||
|
apiUrl: os.Getenv(envWebApiUrl),
|
||||||
}
|
}
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
|
@ -132,37 +224,89 @@ func bootstrapEnvironment() *Environment {
|
||||||
tlsEnabled: serverTlsEnabled,
|
tlsEnabled: serverTlsEnabled,
|
||||||
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
||||||
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
||||||
corsAllowOrigin: []string{os.Getenv(envCorsAllowOrigin)},
|
corsAllowCredentials: os.Getenv(envCorsAllowCredentials) == "true",
|
||||||
|
corsExposeHeaders: []string{os.Getenv(envCorsExposeHeaders)},
|
||||||
|
corsAllowOrigins: []string{os.Getenv(envCorsAllowOrigins)},
|
||||||
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
||||||
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfig := &authConfig{
|
authMode := os.Getenv(envAuthMode)
|
||||||
adminUser: os.Getenv(envAdminUser),
|
|
||||||
adminPassword: os.Getenv(envAdminPassword),
|
if authMode != authModeBasicSingle && authMode != authModeBasicCredentials {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid auth mode. Reason: must be one of ['basic_single','basic_credentials'")
|
||||||
|
}
|
||||||
|
|
||||||
|
authC := &authConfig{
|
||||||
|
authMethod: authMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if authModeBasicSingle == authMode {
|
||||||
|
failIfEnvKeyNotPresent(envBasicAuthUser)
|
||||||
|
failIfEnvKeyNotPresent(envBasicAuthPassword)
|
||||||
|
authC.basicAuthUser = os.Getenv(envBasicAuthUser)
|
||||||
|
authC.basicAuthPassword = os.Getenv(envBasicAuthPassword)
|
||||||
|
}
|
||||||
|
if authModeBasicCredentials == authMode {
|
||||||
|
failIfEnvKeyNotPresent(envBasicAuthCredentials)
|
||||||
|
authC.basicAuthCredentials = parseBasicAuthCredentials(envBasicAuthCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
// task config
|
// task config
|
||||||
var tc *taskConfig
|
var tc *taskConfig
|
||||||
|
|
||||||
var updateCleanStaleMaxAge time.Duration
|
updateCleanStaleInterval := parseDuration(envTaskUpdateCleanStaleInterval)
|
||||||
if updateCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskUpdateCleanStaleMaxAge)); errParse != nil {
|
updateCleanStaleMaxAge := parseDuration(envTaskUpdateCleanStaleMaxAge)
|
||||||
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale updates. Reason: %s", errParse.Error())
|
eventCleanStaleMaxAge := parseDuration(envTaskEventCleanStaleMaxAge)
|
||||||
|
actionsCleanStaleMaxAge := parseDuration(envTaskActionsCleanStaleMaxAge)
|
||||||
|
eventCleanStaleInterval := parseDuration(envTaskEventCleanStaleInterval)
|
||||||
|
actionsEnqueueInterval := parseDuration(envTaskActionsEnqueueInterval)
|
||||||
|
actionsInvokeInterval := parseDuration(envTaskActionsInvokeInterval)
|
||||||
|
actionsCleanStaleInterval := parseDuration(envTaskActionsCleanStaleInterval)
|
||||||
|
prometheusRefreshInterval := parseDuration(envTaskPrometheusRefreshInterval)
|
||||||
|
|
||||||
|
var actionsEnqueueBatchSize int
|
||||||
|
if actionsEnqueueBatchSize, err = strconv.Atoi(os.Getenv(envTaskActionsEnqueueBatchSize)); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid actions enqueue batch size. Reason: %v", err)
|
||||||
|
}
|
||||||
|
if actionsEnqueueBatchSize <= 0 {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid actions enqueue batch size, must be a positive number.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventCleanStaleMaxAge time.Duration
|
var actionsInvokeBatchSize int
|
||||||
if eventCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskEventCleanStaleMaxAge)); errParse != nil {
|
if actionsInvokeBatchSize, err = strconv.Atoi(os.Getenv(envTaskActionsInvokeBatchSize)); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale events. Reason: %s", errParse.Error())
|
zap.L().Sugar().Fatalf("Invalid actions invoke batch size. Reason: %v", err)
|
||||||
|
}
|
||||||
|
if actionsInvokeBatchSize <= 0 {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid actions invoke batch size, must be a positive number.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsInvokeMaxRetries int
|
||||||
|
if actionsInvokeMaxRetries, err = strconv.Atoi(os.Getenv(envTaskActionsInvokeMaxRetries)); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid actions invoke max retries. Reason: %v", err)
|
||||||
|
}
|
||||||
|
if actionsInvokeMaxRetries <= 0 {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid actions invoke max retries, must be a positive number.")
|
||||||
}
|
}
|
||||||
|
|
||||||
tc = &taskConfig{
|
tc = &taskConfig{
|
||||||
updateCleanStaleEnabled: os.Getenv(envTaskUpdateCleanStaleEnabled) == "true",
|
updateCleanStaleEnabled: os.Getenv(envTaskUpdateCleanStaleEnabled) == "true",
|
||||||
updateCleanStaleInterval: os.Getenv(envTaskUpdateCleanStaleInterval),
|
updateCleanStaleInterval: updateCleanStaleInterval,
|
||||||
updateCleanStaleMaxAge: updateCleanStaleMaxAge,
|
updateCleanStaleMaxAge: updateCleanStaleMaxAge,
|
||||||
eventCleanStaleEnabled: os.Getenv(envTaskEventCleanStaleEnabled) == "true",
|
eventCleanStaleEnabled: os.Getenv(envTaskEventCleanStaleEnabled) == "true",
|
||||||
eventCleanStaleInterval: os.Getenv(envTaskEventCleanStaleInterval),
|
eventCleanStaleInterval: eventCleanStaleInterval,
|
||||||
eventCleanStaleMaxAge: eventCleanStaleMaxAge,
|
eventCleanStaleMaxAge: eventCleanStaleMaxAge,
|
||||||
prometheusRefreshInterval: os.Getenv(envTaskPrometheusRefreshInterval),
|
actionsEnqueueEnabled: os.Getenv(envTaskActionsEnqueueEnabled) == "true",
|
||||||
|
actionsEnqueueInterval: actionsEnqueueInterval,
|
||||||
|
actionsEnqueueBatchSize: actionsEnqueueBatchSize,
|
||||||
|
actionsInvokeEnabled: os.Getenv(envTaskActionsInvokeEnabled) == "true",
|
||||||
|
actionsInvokeInterval: actionsInvokeInterval,
|
||||||
|
actionsInvokeBatchSize: actionsInvokeBatchSize,
|
||||||
|
actionsInvokeMaxRetries: actionsInvokeMaxRetries,
|
||||||
|
actionsCleanStaleEnabled: os.Getenv(envTaskActionsCleanStaleEnabled) == "true",
|
||||||
|
actionsCleanStaleInterval: actionsCleanStaleInterval,
|
||||||
|
actionsCleanStaleMaxAge: actionsCleanStaleMaxAge,
|
||||||
|
prometheusRefreshInterval: prometheusRefreshInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
var lc *lockConfig
|
var lc *lockConfig
|
||||||
|
@ -179,24 +323,31 @@ func bootstrapEnvironment() *Environment {
|
||||||
zap.L().Sugar().Fatalln("Invalid webhook token length. Reason: must be a positive number")
|
zap.L().Sugar().Fatalln("Invalid webhook token length. Reason: must be a positive number")
|
||||||
}
|
}
|
||||||
|
|
||||||
webhookConfig := &webhookConfig{
|
wc := &webhookConfig{
|
||||||
tokenLength: webhookTokenLength,
|
tokenLength: webhookTokenLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
prometheusConfig := &prometheusConfig{
|
pc := &prometheusConfig{
|
||||||
enabled: os.Getenv(envPrometheusEnabled) == "true",
|
enabled: os.Getenv(envPrometheusEnabled) == "true",
|
||||||
path: os.Getenv(envPrometheusMetricsPath),
|
path: os.Getenv(envPrometheusMetricsPath),
|
||||||
secureTokenEnabled: os.Getenv(envPrometheusSecureTokenEnabled) == "true",
|
secureTokenEnabled: os.Getenv(envPrometheusSecureTokenEnabled) == "true",
|
||||||
secureToken: os.Getenv(envPrometheusSecureToken),
|
secureToken: os.Getenv(envPrometheusSecureToken),
|
||||||
}
|
}
|
||||||
|
|
||||||
if prometheusConfig.enabled && prometheusConfig.secureTokenEnabled {
|
if pc.enabled && pc.secureTokenEnabled {
|
||||||
failIfEnvKeyNotPresent(envPrometheusSecureToken)
|
failIfEnvKeyNotPresent(envPrometheusSecureToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// database setup
|
// database setup
|
||||||
gormLogger := zapgorm2.New(logger)
|
gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
|
||||||
gormLogger.SetAsDefault()
|
if isDebug && isDevelopment {
|
||||||
|
gormZapLogger := zap.Must(zapConfig.Build())
|
||||||
|
defer func(gormZapLogger *zap.Logger) {
|
||||||
|
_ = gormZapLogger.Sync()
|
||||||
|
}(gormZapLogger)
|
||||||
|
gormLogger := zapgorm2.New(gormZapLogger)
|
||||||
|
gormConfig = &gorm.Config{Logger: gormLogger}
|
||||||
|
}
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
zap.L().Sugar().Infof("Using database type '%s'", os.Getenv(envDbType))
|
zap.L().Sugar().Infof("Using database type '%s'", os.Getenv(envDbType))
|
||||||
|
@ -204,7 +355,7 @@ func bootstrapEnvironment() *Environment {
|
||||||
if os.Getenv(envDbType) == dbTypeSqlite {
|
if os.Getenv(envDbType) == dbTypeSqlite {
|
||||||
if os.Getenv(envDbSqliteFile) == "" {
|
if os.Getenv(envDbSqliteFile) == "" {
|
||||||
var defaultDbFile string
|
var defaultDbFile string
|
||||||
if defaultDbFile, err = xdg.DataFile(Name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
if defaultDbFile, err = xdg.DataFile(name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
||||||
}
|
}
|
||||||
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
||||||
|
@ -213,12 +364,16 @@ func bootstrapEnvironment() *Environment {
|
||||||
dbFile := os.Getenv(envDbSqliteFile)
|
dbFile := os.Getenv(envDbSqliteFile)
|
||||||
zap.L().Sugar().Infof("Using database file '%s'", dbFile)
|
zap.L().Sugar().Infof("Using database file '%s'", dbFile)
|
||||||
|
|
||||||
if db, err = gorm.Open(sqlite.Open(dbFile), &gorm.Config{Logger: gormLogger}); err != nil {
|
if err = util.CreateFileWithParent(dbFile); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Database file '%s' cannot be created: %v", dbFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if db, err = gorm.Open(sqlite.Open(dbFile), gormConfig); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if res := db.Exec("PRAGMA foreign_keys = ON"); res.Error != nil {
|
if res := db.Exec("PRAGMA foreign_keys = ON"); res.Error != nil {
|
||||||
zap.L().Sugar().Fatalf("Could not invoke foreign key for SQLite: %v", res.Error)
|
zap.L().Sugar().Fatalf("Could not execute foreign key for SQLite: %v", res.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlDb, _ := db.DB()
|
sqlDb, _ := db.DB()
|
||||||
|
@ -237,7 +392,7 @@ func bootstrapEnvironment() *Environment {
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=%v", host, dbUser, dbPass, dbName, port, dbTZ)
|
dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=%v", host, dbUser, dbPass, dbName, port, dbTZ)
|
||||||
if db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger}); err != nil {
|
if db, err = gorm.Open(postgres.Open(dsn), gormConfig); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -248,16 +403,17 @@ func bootstrapEnvironment() *Environment {
|
||||||
zap.L().Sugar().Fatalf("Could not setup database")
|
zap.L().Sugar().Fatalf("Could not setup database")
|
||||||
}
|
}
|
||||||
|
|
||||||
env := &Environment{appConfig: appConfig,
|
env := &Environment{appConfig: ac,
|
||||||
authConfig: authConfig,
|
webConfig: webC,
|
||||||
|
authConfig: authC,
|
||||||
serverConfig: sc,
|
serverConfig: sc,
|
||||||
taskConfig: tc,
|
taskConfig: tc,
|
||||||
lockConfig: lc,
|
lockConfig: lc,
|
||||||
webhookConfig: webhookConfig,
|
webhookConfig: wc,
|
||||||
prometheusConfig: prometheusConfig,
|
prometheusConfig: pc,
|
||||||
db: db}
|
db: db}
|
||||||
|
|
||||||
if err = env.db.AutoMigrate(&Update{}, &Webhook{}, &Event{}); err != nil {
|
if err = env.db.AutoMigrate(&Update{}, &Webhook{}, &Event{}, &Secret{}, &Action{}, &ActionInvocation{}); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Could not migrate database schema: %s", err)
|
zap.L().Sugar().Fatalf("Could not migrate database schema: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,11 +426,17 @@ func bootstrapEnvironment() *Environment {
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapFromEnvironmentAndValidate() {
|
func bootstrapFromEnvironmentAndValidate() {
|
||||||
|
failIfEnvKeyNotPresent(envSecret)
|
||||||
|
|
||||||
|
// auth mode
|
||||||
|
setEnvKeyDefault(envAuthMode, authModeDefault)
|
||||||
|
|
||||||
// app
|
// app
|
||||||
setEnvKeyDefault(envTZ, tzDefault)
|
setEnvKeyDefault(envTZ, tzDefault)
|
||||||
|
|
||||||
failIfEnvKeyNotPresent(envAdminUser)
|
// web
|
||||||
failIfEnvKeyNotPresent(envAdminPassword)
|
setEnvKeyDefault(envWebTitle, webTitleDefault)
|
||||||
|
setEnvKeyDefault(envWebApiUrl, webApiUrlDefault)
|
||||||
|
|
||||||
// webhook
|
// webhook
|
||||||
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
||||||
|
@ -291,6 +453,19 @@ func bootstrapFromEnvironmentAndValidate() {
|
||||||
setEnvKeyDefault(envTaskEventCleanStaleInterval, taskEventCleanStaleIntervalDefault)
|
setEnvKeyDefault(envTaskEventCleanStaleInterval, taskEventCleanStaleIntervalDefault)
|
||||||
setEnvKeyDefault(envTaskEventCleanStaleMaxAge, taskEventCleanStaleMaxAgeDefault)
|
setEnvKeyDefault(envTaskEventCleanStaleMaxAge, taskEventCleanStaleMaxAgeDefault)
|
||||||
|
|
||||||
|
setEnvKeyDefault(envTaskActionsEnqueueEnabled, taskActionsEnqueueEnabledDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsEnqueueInterval, taskActionsEnqueueIntervalDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsEnqueueBatchSize, taskActionsEnqueueBatchSizeDefault)
|
||||||
|
|
||||||
|
setEnvKeyDefault(envTaskActionsInvokeEnabled, taskActionsInvokeEnabledDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsInvokeInterval, taskActionsInvokeIntervalDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsInvokeBatchSize, taskActionsInvokeBatchSizeDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsInvokeMaxRetries, taskActionsInvokeMaxRetriesDefault)
|
||||||
|
|
||||||
|
setEnvKeyDefault(envTaskActionsCleanStaleEnabled, taskActionsCleanStaleEnabledDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsCleanStaleInterval, taskActionsCleanStaleIntervalDefault)
|
||||||
|
setEnvKeyDefault(envTaskActionsCleanStaleMaxAge, taskActionsCleanStaleMaxAgeDefault)
|
||||||
|
|
||||||
setEnvKeyDefault(envTaskPrometheusRefreshInterval, taskPrometheusRefreshDefault)
|
setEnvKeyDefault(envTaskPrometheusRefreshInterval, taskPrometheusRefreshDefault)
|
||||||
|
|
||||||
// prometheus
|
// prometheus
|
||||||
|
@ -311,9 +486,11 @@ func bootstrapFromEnvironmentAndValidate() {
|
||||||
setEnvKeyDefault(envServerPort, serverPortDefault)
|
setEnvKeyDefault(envServerPort, serverPortDefault)
|
||||||
setEnvKeyDefault(envServerListen, serverListenDefault)
|
setEnvKeyDefault(envServerListen, serverListenDefault)
|
||||||
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
||||||
setEnvKeyDefault(envCorsAllowOrigin, corsAllowOriginDefault)
|
setEnvKeyDefault(envCorsAllowOrigins, corsAllowOriginsDefault)
|
||||||
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
||||||
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
||||||
|
setEnvKeyDefault(envCorsAllowCredentials, corsAllowCredentialsDefault)
|
||||||
|
setEnvKeyDefault(envCorsExposeHeaders, corsExposeHeadersDefault)
|
||||||
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,3 +510,46 @@ func setEnvKeyDefault(key string, defaultValue string) {
|
||||||
zap.L().Sugar().Infof("Set '%s' to '%s'", key, defaultValue)
|
zap.L().Sugar().Infof("Set '%s' to '%s'", key, defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseDuration(envProperty string) time.Duration {
|
||||||
|
var duration time.Duration
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if duration, err = time.ParseDuration(os.Getenv(envProperty)); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not parse duration for '%s'. Reason: %s", envProperty, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBasicAuthCredentials(envProperty string) map[string]string {
|
||||||
|
if envProperty == "" {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid env for parsing basic auth credentials")
|
||||||
|
}
|
||||||
|
credentialsFromEnv := os.Getenv(envProperty)
|
||||||
|
|
||||||
|
var credentials []string
|
||||||
|
credentials = strings.Split(credentialsFromEnv, ",")
|
||||||
|
|
||||||
|
basicAuthCredentials := make(map[string]string)
|
||||||
|
|
||||||
|
for _, c := range credentials {
|
||||||
|
pair := strings.Split(c, "=")
|
||||||
|
|
||||||
|
if len(pair) != 2 {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: credentials must be specified with the = separator per credential entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair[0] == "" {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: username must not be blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair[1] == "" {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: password must not be blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
basicAuthCredentials[pair[0]] = pair[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return basicAuthCredentials
|
||||||
|
}
|
||||||
|
|
|
@ -6,40 +6,44 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errorValidationNotEmpty = newServiceError(IllegalArgument, errors.New("assert: empty values are not allowed"))
|
errorValidationNotEmpty = newServiceError(illegalArgument, errors.New("assert: empty values are not allowed"))
|
||||||
errorValidationNotBlank = newServiceError(IllegalArgument, errors.New("assert: blank values are not allowed"))
|
errorValidationNotBlank = newServiceError(illegalArgument, errors.New("assert: blank values are not allowed"))
|
||||||
errorValidationPageGreaterZero = newServiceError(IllegalArgument, errors.New("assert: page has to be greater 0"))
|
errorValidationPageGreaterZero = newServiceError(illegalArgument, errors.New("assert: page has to be greater 0"))
|
||||||
errorValidationPageSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
errorValidationPageSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
||||||
|
errorValidationLimitGreaterZero = newServiceError(illegalArgument, errors.New("assert: limit has to be greater 0"))
|
||||||
|
errorValidationSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: size has to be greater 0"))
|
||||||
|
errorValidationMaxRetriesGreaterZero = newServiceError(illegalArgument, errors.New("assert: max retries has to be greater 0"))
|
||||||
|
|
||||||
errorResourceNotFound = newServiceError(NotFound, errors.New("resource not found"))
|
errorResourceNotFound = newServiceError(notFound, errors.New("resource not found"))
|
||||||
errorResourceAccessDenied = newServiceError(Forbidden, errors.New("resource access denied"))
|
errorResourceAccessDenied = newServiceError(forbidden, errors.New("resource access denied"))
|
||||||
|
|
||||||
errorDatabaseRowsExpected = newServiceDatabaseError(errors.New("action failed, expected affected rows, but got none"))
|
errorDatabaseRowsExpected = newServiceDatabaseError(errors.New("action failed, expected affected rows, but got none"))
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrorCode string
|
type errorCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
IllegalArgument ErrorCode = "IllegalArgument"
|
illegalArgument errorCode = "IllegalArgument"
|
||||||
Unauthorized ErrorCode = "Unauthorized"
|
unauthorized errorCode = "Unauthorized"
|
||||||
Forbidden ErrorCode = "Forbidden"
|
forbidden errorCode = "Forbidden"
|
||||||
NotFound ErrorCode = "NotFound"
|
notFound errorCode = "NotFound"
|
||||||
Conflict ErrorCode = "Conflict"
|
methodNotAllowed errorCode = "MethodNotAllowed"
|
||||||
General ErrorCode = "General"
|
conflict errorCode = "Conflict"
|
||||||
|
general errorCode = "General"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newServiceError returns an error that formats as the given text and aligns with builtin error
|
// newServiceError returns an error that formats as the given text and aligns with builtin error
|
||||||
func newServiceError(status ErrorCode, err error) error {
|
func newServiceError(status errorCode, err error) error {
|
||||||
return &serviceError{status, fmt.Errorf("service error (%v): %w", status, err)}
|
return &serviceError{status, fmt.Errorf("service error (%v): %w", status, err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newServiceDatabaseError returns an error that formats as the given text and aligns with builtin error
|
// newServiceDatabaseError returns an error that formats as the given text and aligns with builtin error
|
||||||
func newServiceDatabaseError(error error) error {
|
func newServiceDatabaseError(error error) error {
|
||||||
return newServiceError(General, fmt.Errorf("database error: %w", error))
|
return newServiceError(general, fmt.Errorf("database error: %w", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceError struct {
|
type serviceError struct {
|
||||||
Status ErrorCode
|
Status errorCode
|
||||||
Cause error
|
Cause error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
375
server/repository_action.go
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionRepository interface {
|
||||||
|
paginate(page int, pageSize int, orderBy string, order string) ([]*Action, error)
|
||||||
|
count() (int64, error)
|
||||||
|
find(id string) (*Action, error)
|
||||||
|
findByEnabled(enabled bool) ([]*Action, error)
|
||||||
|
findAll() ([]*Action, error)
|
||||||
|
create(label string, t api.ActionType, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}, enabled bool) (*Action, error)
|
||||||
|
updateLabel(id string, label string) (*Action, error)
|
||||||
|
updateMatchEvent(id string, matchEvent *string) (*Action, error)
|
||||||
|
updateMatchApplication(id string, matchApplication *string) (*Action, error)
|
||||||
|
updateMatchProvider(id string, matchProvider *string) (*Action, error)
|
||||||
|
updateMatchHost(id string, matchHost *string) (*Action, error)
|
||||||
|
updateTypeAndPayload(id string, t api.ActionType, payload interface{}) (*Action, error)
|
||||||
|
updateEnabled(id string, enabled bool) (*Action, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionDbRepo(db *gorm.DB) *actionDbRepo {
|
||||||
|
return &actionDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) find(id string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Action
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Find(&e, "id = ?", id); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) findByEnabled(enabled bool) ([]*Action, error) {
|
||||||
|
var e []*Action
|
||||||
|
|
||||||
|
res := r.db.Find(&e, "enabled = ?", enabled)
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) create(label string, t api.ActionType, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}, enabled bool) (*Action, error) {
|
||||||
|
if label == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &Action{
|
||||||
|
Label: label,
|
||||||
|
Type: t.Value(),
|
||||||
|
MatchEvent: matchEvent,
|
||||||
|
MatchHost: matchHost,
|
||||||
|
MatchApplication: matchApplication,
|
||||||
|
MatchProvider: matchProvider,
|
||||||
|
Enabled: enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
unmarshalledPayload := JSONMap{}
|
||||||
|
marshalledMetadata, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = unmarshalledPayload.UnmarshalJSON(marshalledMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.Payload = unmarshalledPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Create(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateLabel(id string, label string) (*Action, error) {
|
||||||
|
if id == "" || label == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Label = label
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateType(id string, t api.ActionType) (*Action, error) {
|
||||||
|
if id == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Type = t.Value()
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateMatchEvent(id string, matchEvent *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.MatchEvent = matchEvent
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateMatchApplication(id string, matchApplication *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.MatchApplication = matchApplication
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateMatchProvider(id string, matchProvider *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.MatchProvider = matchProvider
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateMatchHost(id string, matchHost *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.MatchHost = matchHost
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateTypeAndPayload(id string, t api.ActionType, payload interface{}) (*Action, error) {
|
||||||
|
if id == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
if payload == nil {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarshalledPayload := JSONMap{}
|
||||||
|
marshalledMetadata, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = unmarshalledPayload.UnmarshalJSON(marshalledMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.Payload = unmarshalledPayload
|
||||||
|
e.Type = t.Value()
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) updateEnabled(id string, enabled bool) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Enabled = enabled
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&Action{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) paginate(page int, pageSize int, orderBy string, order string) ([]*Action, error) {
|
||||||
|
if page == 0 {
|
||||||
|
return nil, errorValidationPageGreaterZero
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
return nil, errorValidationPageSizeGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var e []*Action
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if orderBy != "" && order != "" {
|
||||||
|
res = r.db.Order(orderBy + " " + order).Offset(offset).Limit(pageSize).Find(&e)
|
||||||
|
} else {
|
||||||
|
res = r.db.Offset(offset).Limit(pageSize).Find(&e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) count() (int64, error) {
|
||||||
|
var c int64
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Model(&Action{}).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionDbRepo) findAll() ([]*Action, error) {
|
||||||
|
var e []*Action
|
||||||
|
|
||||||
|
if res := r.db.Model(&Action{}).Order("updated_at desc").Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
265
server/repository_action_invocation.go
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionInvocationRepository interface {
|
||||||
|
paginate(page int, pageSize int, orderBy string, order string) ([]*ActionInvocation, error)
|
||||||
|
count() (int64, error)
|
||||||
|
find(id string) (*ActionInvocation, error)
|
||||||
|
findAllByState(limit int, maxRetries int, state ...api.ActionInvocationState) ([]*ActionInvocation, error)
|
||||||
|
create(eventId string, actionId string, state api.ActionInvocationState) (*ActionInvocation, error)
|
||||||
|
updateState(id string, state api.ActionInvocationState) (*ActionInvocation, error)
|
||||||
|
updateMessage(id string, message *string) (*ActionInvocation, error)
|
||||||
|
updateRetryCount(id string, retryCount int) (*ActionInvocation, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
deleteByUpdatedAtBeforeAndStates(time time.Time, retryCount int, state ...api.ActionInvocationState) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionInvocationDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionInvocationDbRepo(db *gorm.DB) *actionInvocationDbRepo {
|
||||||
|
return &actionInvocationDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) find(id string) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e ActionInvocation
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Find(&e, "id = ?", id); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) create(eventId string, actionId string, state api.ActionInvocationState) (*ActionInvocation, error) {
|
||||||
|
if eventId == "" || actionId == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &ActionInvocation{
|
||||||
|
EventID: eventId,
|
||||||
|
ActionID: actionId,
|
||||||
|
State: state.Value(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Create(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) updateRetryCount(id string, retryCount int) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *ActionInvocation
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.RetryCount = retryCount
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) updateState(id string, state api.ActionInvocationState) (*ActionInvocation, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *ActionInvocation
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.State = state.Value()
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) updateMessage(id string, message *string) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *ActionInvocation
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Message = message
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&ActionInvocation{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) paginate(page int, pageSize int, orderBy string, order string) ([]*ActionInvocation, error) {
|
||||||
|
if page == 0 {
|
||||||
|
return nil, errorValidationPageGreaterZero
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
return nil, errorValidationPageSizeGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var e []*ActionInvocation
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if orderBy != "" && order != "" {
|
||||||
|
res = r.db.Order(orderBy + " " + order).Offset(offset).Limit(pageSize).Find(&e)
|
||||||
|
} else {
|
||||||
|
res = r.db.Offset(offset).Limit(pageSize).Find(&e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) count() (int64, error) {
|
||||||
|
var c int64
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Model(&ActionInvocation{}).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) findAllByState(limit int, maxRetries int, state ...api.ActionInvocationState) ([]*ActionInvocation, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
return nil, errorValidationLimitGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
var e []*ActionInvocation
|
||||||
|
|
||||||
|
states := translateActionInvocationState(state...)
|
||||||
|
|
||||||
|
if res := r.db.Model(&ActionInvocation{}).Scopes(allGetActionInvocationCriterion(states, maxRetries)).Order("created_at asc").Limit(limit).Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *actionInvocationDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, maxRetries int, state ...api.ActionInvocationState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
states := translateActionInvocationState(state...)
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Where("retry_count >= ?", maxRetries).Where("state IN ?", states).Where("updated_at < ?", time).Delete(&ActionInvocation{}); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateActionInvocationState(state ...api.ActionInvocationState) []string {
|
||||||
|
states := make([]string, 0, len(state))
|
||||||
|
if len(state) > 0 {
|
||||||
|
for _, s := range state {
|
||||||
|
states = append(states, s.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
|
func criterionActonInvocationMaxRetries(maxRetries int) func(db *gorm.DB) *gorm.DB {
|
||||||
|
if maxRetries > 0 {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("retry_count < ? ", maxRetries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func criterionActionInvocationState(states []string) func(db *gorm.DB) *gorm.DB {
|
||||||
|
if states != nil && len(states) > 0 {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("state IN (?)", states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allGetActionInvocationCriterion(states []string, maxRetries int) func(db *gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Scopes(criterionActionInvocationState(states), criterionActonInvocationMaxRetries(maxRetries))
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ type eventRepository interface {
|
||||||
window(size int, skip int, orderBy string, order string) ([]*Event, error)
|
window(size int, skip int, orderBy string, order string) ([]*Event, error)
|
||||||
windowHasNext(size int, skip int, orderBy string, order string) (bool, error)
|
windowHasNext(size int, skip int, orderBy string, order string) (bool, error)
|
||||||
count(state ...api.EventState) (int64, error)
|
count(state ...api.EventState) (int64, error)
|
||||||
|
findAllByState(limit int, state ...api.EventState) ([]*Event, error)
|
||||||
create(name api.EventName, state api.EventState, payload interface{}) (*Event, error)
|
create(name api.EventName, state api.EventState, payload interface{}) (*Event, error)
|
||||||
|
updateState(id string, state api.EventState) (*Event, error)
|
||||||
delete(id string) (int64, error)
|
delete(id string) (int64, error)
|
||||||
deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.EventState) (int64, error)
|
deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.EventState) (int64, error)
|
||||||
}
|
}
|
||||||
|
@ -81,6 +83,31 @@ func (r *eventDbRepo) create(name api.EventName, state api.EventState, payload i
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) updateState(id string, state api.EventState) (*Event, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Event
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.State = state.Value()
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *eventDbRepo) delete(id string) (int64, error) {
|
func (r *eventDbRepo) delete(id string) (int64, error) {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return 0, errorValidationNotBlank
|
return 0, errorValidationNotBlank
|
||||||
|
@ -98,7 +125,7 @@ func (r *eventDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ...
|
||||||
return 0, errorValidationNotEmpty
|
return 0, errorValidationNotEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
states := make([]string, 0)
|
states := make([]string, 0, len(state))
|
||||||
for _, i := range state {
|
for _, i := range state {
|
||||||
states = append(states, i.Value())
|
states = append(states, i.Value())
|
||||||
}
|
}
|
||||||
|
@ -112,8 +139,9 @@ func (r *eventDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ...
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *eventDbRepo) window(size int, skip int, orderBy string, order string) ([]*Event, error) {
|
func (r *eventDbRepo) window(size int, skip int, orderBy string, order string) ([]*Event, error) {
|
||||||
var e []*Event
|
if size <= 0 {
|
||||||
|
return nil, errorValidationSizeGreaterZero
|
||||||
|
}
|
||||||
if orderBy == "" {
|
if orderBy == "" {
|
||||||
orderBy = "created_at"
|
orderBy = "created_at"
|
||||||
}
|
}
|
||||||
|
@ -121,6 +149,7 @@ func (r *eventDbRepo) window(size int, skip int, orderBy string, order string) (
|
||||||
order = "asc"
|
order = "asc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var e []*Event
|
||||||
if res := r.db.Order(orderBy + " " + order).Offset(skip).Limit(size).Find(&e); res.Error != nil {
|
if res := r.db.Order(orderBy + " " + order).Offset(skip).Limit(size).Find(&e); res.Error != nil {
|
||||||
return nil, newServiceDatabaseError(res.Error)
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
}
|
}
|
||||||
|
@ -145,9 +174,37 @@ func (r *eventDbRepo) windowHasNext(size int, skip int, orderBy string, order st
|
||||||
return len(e) > 0, nil
|
return len(e) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) findAllByState(limit int, state ...api.EventState) ([]*Event, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return nil, errorValidationLimitGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
var e []*Event
|
||||||
|
|
||||||
|
states := translateEventState(state...)
|
||||||
|
|
||||||
|
if res := r.db.Model(&Event{}).Scopes(allGetEventCriterion(states)).Order("created_at asc").Limit(limit).Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *eventDbRepo) count(state ...api.EventState) (int64, error) {
|
func (r *eventDbRepo) count(state ...api.EventState) (int64, error) {
|
||||||
var c int64
|
var c int64
|
||||||
|
|
||||||
|
states := translateEventState(state...)
|
||||||
|
if res := r.db.Model(&Event{}).Scopes(allGetEventCriterion(states)).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func translateEventState(state ...api.EventState) []string {
|
||||||
states := make([]string, 0)
|
states := make([]string, 0)
|
||||||
if len(state) > 0 {
|
if len(state) > 0 {
|
||||||
for _, s := range state {
|
for _, s := range state {
|
||||||
|
@ -155,11 +212,7 @@ func (r *eventDbRepo) count(state ...api.EventState) (int64, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if res := r.db.Model(&Event{}).Scopes(allGetEventCriterion(states)).Count(&c); res.Error != nil {
|
return states
|
||||||
return 0, newServiceDatabaseError(res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func criterionEventState(states []string) func(db *gorm.DB) *gorm.DB {
|
func criterionEventState(states []string) func(db *gorm.DB) *gorm.DB {
|
||||||
|
|
131
server/repository_secret.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type secretRepository interface {
|
||||||
|
findAll() ([]*Secret, error)
|
||||||
|
findById(id string) (*Secret, error)
|
||||||
|
findByKey(key string) (*Secret, error)
|
||||||
|
create(key string, value string) (*Secret, error)
|
||||||
|
update(id string, value string) (*Secret, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecretDbRepo(db *gorm.DB) *secretDbRepo {
|
||||||
|
return &secretDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) findAll() ([]*Secret, error) {
|
||||||
|
var e []*Secret
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Order("key asc").Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) findById(id string) (*Secret, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Secret
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Find(&e, "id = ?", id); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) findByKey(key string) (*Secret, error) {
|
||||||
|
if key == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Secret
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Find(&e, "key = ?", key); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) create(key string, value string) (*Secret, error) {
|
||||||
|
if key == "" || value == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Secret
|
||||||
|
|
||||||
|
e = &Secret{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Create(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return nil, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) update(id string, value string) (*Secret, error) {
|
||||||
|
if id == "" || value == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Secret
|
||||||
|
|
||||||
|
if e, err = r.findById(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Value = value
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Save(&e); res.Error != nil {
|
||||||
|
return nil, res.Error
|
||||||
|
}
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
return e, errorDatabaseRowsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *secretDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&Secret{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
|
@ -199,7 +199,7 @@ func (r *updateDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ..
|
||||||
return 0, errorValidationNotEmpty
|
return 0, errorValidationNotEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
states := make([]string, 0)
|
states := make([]string, 0, len(state))
|
||||||
for _, i := range state {
|
for _, i := range state {
|
||||||
states = append(states, i.Value())
|
states = append(states, i.Value())
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ func (r *updateDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ..
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order string, searchTerm string, searchIn string, state ...api.UpdateState) ([]*Update, error) {
|
func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order string, searchTerm string, searchIn string, state ...api.UpdateState) ([]*Update, error) {
|
||||||
if page == 0 || pageSize <= 0 {
|
if page == 0 {
|
||||||
return nil, errorValidationPageGreaterZero
|
return nil, errorValidationPageGreaterZero
|
||||||
}
|
}
|
||||||
if pageSize <= 0 {
|
if pageSize <= 0 {
|
||||||
|
@ -231,7 +231,7 @@ func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order st
|
||||||
order = "desc"
|
order = "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
states := make([]string, 0)
|
states := make([]string, 0, len(state))
|
||||||
if len(state) > 0 {
|
if len(state) > 0 {
|
||||||
for _, s := range state {
|
for _, s := range state {
|
||||||
states = append(states, s.Value())
|
states = append(states, s.Value())
|
||||||
|
@ -248,7 +248,7 @@ func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order st
|
||||||
func (r *updateDbRepo) count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error) {
|
func (r *updateDbRepo) count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error) {
|
||||||
var c int64
|
var c int64
|
||||||
|
|
||||||
states := make([]string, 0)
|
states := make([]string, 0, len(state))
|
||||||
if len(state) > 0 {
|
if len(state) > 0 {
|
||||||
for _, s := range state {
|
for _, s := range state {
|
||||||
states = append(states, s.Value())
|
states = append(states, s.Value())
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebhookRepository interface {
|
type webhookRepository interface {
|
||||||
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
||||||
count() (int64, error)
|
count() (int64, error)
|
||||||
find(id string) (*Webhook, error)
|
find(id string) (*Webhook, error)
|
||||||
|
@ -129,7 +129,7 @@ func (r *webhookDbRepo) delete(id string) (int64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *webhookDbRepo) paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error) {
|
func (r *webhookDbRepo) paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error) {
|
||||||
if page == 0 || pageSize <= 0 {
|
if page == 0 {
|
||||||
return nil, errorValidationPageGreaterZero
|
return nil, errorValidationPageGreaterZero
|
||||||
}
|
}
|
||||||
if pageSize <= 0 {
|
if pageSize <= 0 {
|
||||||
|
|
264
server/service_action.go
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionService struct {
|
||||||
|
repo ActionRepository
|
||||||
|
eventService *eventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionService(r ActionRepository, e *eventService) *actionService {
|
||||||
|
return &actionService{
|
||||||
|
repo: r,
|
||||||
|
eventService: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) get(id string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := s.repo.find(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) create(label string, t api.ActionType, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}, enabled bool) (*Action, error) {
|
||||||
|
if label == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||||
|
return nil, newServiceError(illegalArgument, validationErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Action
|
||||||
|
if e, err = s.repo.create(label, t, matchEvent, matchHost, matchApplication, matchProvider, payload, enabled); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
zap.L().Sugar().Info("Created action")
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) isValidPayload(t api.ActionType, payload interface{}) (bool, error) {
|
||||||
|
if t == "" {
|
||||||
|
return false, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
if payload == nil {
|
||||||
|
return false, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if api.ActionTypeShoutrrr == t {
|
||||||
|
var pb []byte
|
||||||
|
if pb, err = json.Marshal(payload); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var p actionPayloadShoutrrrDto
|
||||||
|
if err = json.Unmarshal(pb, &p); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
valid := validator.New()
|
||||||
|
if err = valid.Struct(p); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateLabel(id string, label string) (*Action, error) {
|
||||||
|
if id == "" || label == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateLabel(id, label); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateMatchEvent(id string, matchEvent *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateMatchEvent(id, matchEvent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateMatchApplication(id string, matchApplication *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateMatchApplication(id, matchApplication); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateMatchProvider(id string, matchProvider *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateMatchProvider(id, matchProvider); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateMatchHost(id string, matchHost *string) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateMatchHost(id, matchHost); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateTypeAndPayload(id string, t api.ActionType, payload interface{}) (*Action, error) {
|
||||||
|
if id == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
if payload == nil {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||||
|
return nil, newServiceError(illegalArgument, validationErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateTypeAndPayload(id, t, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) updateEnabled(id string, enabled bool) (*Action, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Action
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateEnabled(id, enabled); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) delete(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := s.get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.repo.delete(e.ID.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Deleted action '%v'", id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) paginate(page int, pageSize int, orderBy string, order string) ([]*Action, error) {
|
||||||
|
return s.repo.paginate(page, pageSize, orderBy, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) count() (int64, error) {
|
||||||
|
return s.repo.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) getAll() ([]*Action, error) {
|
||||||
|
return s.repo.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionService) getByEnabled(enabled bool) ([]*Action, error) {
|
||||||
|
return s.repo.findByEnabled(enabled)
|
||||||
|
}
|
404
server/service_action_invocation.go
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
|
"github.com/containrrr/shoutrrr"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionInvocationService struct {
|
||||||
|
repo ActionInvocationRepository
|
||||||
|
actionService *actionService
|
||||||
|
eventService *eventService
|
||||||
|
secretService *secretService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newActionInvocationService(r ActionInvocationRepository, a *actionService, e *eventService, s *secretService) *actionInvocationService {
|
||||||
|
return &actionInvocationService{
|
||||||
|
repo: r,
|
||||||
|
actionService: a,
|
||||||
|
eventService: e,
|
||||||
|
secretService: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) enqueue(batchSize int) error {
|
||||||
|
if batchSize <= 0 {
|
||||||
|
return newServiceError(general, errors.New("cannot enqueue actions from events with invalid configured batch size"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []*Event
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if events, err = s.eventService.getByState(batchSize, api.EventStateCreated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions []*Action
|
||||||
|
if actions, err = s.actionService.getByEnabled(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
if err = s.enqueueFromEvent(event, actions); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not enqueue action for event '%s' (%s). Reason: %s", event.Name, event.ID, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Action) error {
|
||||||
|
if event == nil || actions == nil {
|
||||||
|
return newServiceError(illegalArgument, errorValidationNotEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// match requires event payload
|
||||||
|
var eventPayload *eventPayloadInformationDto
|
||||||
|
if eventPayload, err = s.eventService.extractPayloadInfo(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredActions := make([]*Action, 0)
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
matchesEvent := action.MatchEvent == nil || *action.MatchEvent == event.Name
|
||||||
|
matchesHost := action.MatchHost == nil || *action.MatchHost == eventPayload.Host
|
||||||
|
matchesApplication := action.MatchApplication == nil || *action.MatchApplication == eventPayload.Application
|
||||||
|
matchesProvider := action.MatchProvider == nil || *action.MatchProvider == eventPayload.Provider
|
||||||
|
|
||||||
|
if matchesEvent && matchesHost && matchesApplication && matchesProvider {
|
||||||
|
filteredActions = append(filteredActions, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filteredActions) == 0 {
|
||||||
|
zap.L().Sugar().Debugf("No actions found which match event '%s', nothing to enqueue", event.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range filteredActions {
|
||||||
|
if _, err = s.create(event, action, api.ActionInvocationStateCreated); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not enqueue action '%s' (%v). Reason: %s", action.Label, action.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark event as enqueued
|
||||||
|
if _, err = s.eventService.updateState(event.ID.String(), api.EventStateEnqueued); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) invoke(batchSize int, maxRetries int) error {
|
||||||
|
if batchSize <= 0 {
|
||||||
|
return newServiceError(general, errors.New("cannot invoke actions with invalid configured batch size"))
|
||||||
|
}
|
||||||
|
if maxRetries <= 0 {
|
||||||
|
return newServiceError(general, errors.New("cannot invoke actions with invalid configured max retries"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var actionInvocations []*ActionInvocation
|
||||||
|
|
||||||
|
if actionInvocations, err = s.getByState(batchSize, maxRetries, api.ActionInvocationStateCreated, api.ActionInvocationStateRetrying); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actionInvocations) == 0 {
|
||||||
|
zap.L().Sugar().Debugf("No action invocations found to process")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, actionInvocation := range actionInvocations {
|
||||||
|
if _, err = s.updateState(actionInvocation.ID.String(), api.ActionInvocationStateRunning); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not mark action invocation '%v' as running. Reason: %s", actionInvocation.ID, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Debugf("Invoking action '%v' for event '%v'", actionInvocation.ActionID, actionInvocation.EventID)
|
||||||
|
|
||||||
|
var event *Event
|
||||||
|
if event, err = s.eventService.get(actionInvocation.EventID); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not find event '%v' for action '%v' and action invocation '%v'. Reason: %s", actionInvocation.EventID, actionInvocation.ActionID, actionInvocation.ID, err.Error())
|
||||||
|
// with cascade, cannot happen
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventPayload *eventPayloadInformationDto
|
||||||
|
if eventPayload, err = s.eventService.extractPayloadInfo(event); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not extract event's '%v' information for action '%v' and action invocation '%v'. Reason: %s", actionInvocation.EventID, actionInvocation.ActionID, actionInvocation.ID, err.Error())
|
||||||
|
// with layout of attached payload, cannot happen
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var action *Action
|
||||||
|
if action, err = s.actionService.get(actionInvocation.ActionID); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not find action '%v' for action invocation '%v'. Reason: %s", actionInvocation.ActionID, actionInvocation.ID, err.Error())
|
||||||
|
// with cascade, cannot happen
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.execute(action, eventPayload); err != nil {
|
||||||
|
var cause error
|
||||||
|
cause = err
|
||||||
|
|
||||||
|
zap.L().Sugar().Errorf("Could not invoke action '%s' (%v) for action invocation '%v'. Reason: %s", action.Label, action.ID, actionInvocation.ID, err.Error())
|
||||||
|
|
||||||
|
var newState api.ActionInvocationState
|
||||||
|
newRetryCount := actionInvocation.RetryCount + 1
|
||||||
|
newState = api.ActionInvocationStateRetrying
|
||||||
|
|
||||||
|
if newRetryCount >= maxRetries {
|
||||||
|
zap.L().Sugar().Infof("Action invocation '%v' exceeded max retry count of '%d'. Not trying again.", actionInvocation.ID, newRetryCount)
|
||||||
|
newState = api.ActionInvocationStateError
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.updateState(actionInvocation.ID.String(), newState); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not mark action invocation '%v' as '%v'. Reason: %s", actionInvocation.ID, newState, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.updateRetryCount(actionInvocation.ID.String(), newRetryCount); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not update action invocation '%v' retry count to '%d'. Reason: %s", actionInvocation.ID, newRetryCount, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := cause.Error()
|
||||||
|
if _, err = s.updateMessage(actionInvocation.ID.String(), &msg); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not update action invocation '%v' message. Reason: %s", actionInvocation.ID, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Debugf("Processed action invocation '%v' for event '%s' (%v) and action '%s' (%v)", actionInvocation.ID, event.Name, event.ID, action.Label, action.ID)
|
||||||
|
if _, err = s.updateState(actionInvocation.ID.String(), api.ActionInvocationStateSuccess); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not mark action invocation '%v' as success. Reason: %s", actionInvocation.ID, err.Error())
|
||||||
|
}
|
||||||
|
if _, err = s.updateMessage(actionInvocation.ID.String(), nil); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not update action invocation '%v' message. Reason: %s", actionInvocation.ID, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) execute(action *Action, eventPayloadInfo *eventPayloadInformationDto) error {
|
||||||
|
if action == nil || eventPayloadInfo == nil {
|
||||||
|
return errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var bytes []byte
|
||||||
|
|
||||||
|
if bytes, err = action.Payload.MarshalJSON(); err != nil {
|
||||||
|
return newServiceError(general, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action.Type {
|
||||||
|
case api.ActionTypeShoutrrr.Value():
|
||||||
|
var payload actionPayloadShoutrrrDto
|
||||||
|
if payload, err = util.UnmarshalGenericJSON[actionPayloadShoutrrrDto](bytes); err != nil {
|
||||||
|
return newServiceError(general, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := s.replaceVars(payload.Body, eventPayloadInfo)
|
||||||
|
body = s.replaceSecrets(body)
|
||||||
|
|
||||||
|
for _, url := range payload.Urls {
|
||||||
|
url = s.replaceSecrets(url)
|
||||||
|
url = s.replaceVars(url, eventPayloadInfo)
|
||||||
|
if err = shoutrrr.Send(url, body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return newServiceError(general, errors.New("no matching action type found for invocation"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) replaceSecrets(str string) string {
|
||||||
|
if str == "" {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches [][]string
|
||||||
|
|
||||||
|
matches = util.ExtractBetween(str, "<SECRET>", "</SECRET>")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
var val string
|
||||||
|
if val, err = s.secretService.getValueByKey(match[1]); err != nil {
|
||||||
|
zap.L().Sugar().Warnf("Could not inject secret '%s'. Reason: %s", match[1], err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
str = strings.ReplaceAll(str, match[0], val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) replaceVars(str string, eventPayloadInfo *eventPayloadInformationDto) string {
|
||||||
|
if str == "" || eventPayloadInfo == nil {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
str = strings.ReplaceAll(str, "<VAR>APPLICATION</VAR>", eventPayloadInfo.Application)
|
||||||
|
str = strings.ReplaceAll(str, "<VAR>PROVIDER</VAR>", eventPayloadInfo.Provider)
|
||||||
|
str = strings.ReplaceAll(str, "<VAR>HOST</VAR>", eventPayloadInfo.Host)
|
||||||
|
str = strings.ReplaceAll(str, "<VAR>VERSION</VAR>", eventPayloadInfo.Version)
|
||||||
|
str = strings.ReplaceAll(str, "<VAR>STATE</VAR>", eventPayloadInfo.State)
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) paginate(page int, pageSize int, orderBy string, order string) ([]*ActionInvocation, error) {
|
||||||
|
return s.repo.paginate(page, pageSize, orderBy, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) get(id string) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := s.repo.find(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) getByState(limit int, maxRetries int, state ...api.ActionInvocationState) ([]*ActionInvocation, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return nil, errorValidationLimitGreaterZero
|
||||||
|
}
|
||||||
|
if maxRetries <= 0 {
|
||||||
|
return nil, errorValidationMaxRetriesGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.findAllByState(limit, maxRetries, state...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) count() (int64, error) {
|
||||||
|
return s.repo.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) delete(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := s.get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.repo.delete(e.ID.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Deleted action '%v'", id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) updateState(id string, state api.ActionInvocationState) (*ActionInvocation, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *ActionInvocation
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateState(id, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action invocation '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) updateMessage(id string, message *string) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *ActionInvocation
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateMessage(id, message); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action invocation '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) updateRetryCount(id string, retryCount int) (*ActionInvocation, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *ActionInvocation
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateRetryCount(id, retryCount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified action invocation '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) create(event *Event, action *Action, state api.ActionInvocationState) (*ActionInvocation, error) {
|
||||||
|
if state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
if action == nil || event == nil {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *ActionInvocation
|
||||||
|
if e, err = s.repo.create(event.ID.String(), action.ID.String(), state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
zap.L().Sugar().Info("Created action invocation")
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *actionInvocationService) cleanStale(time time.Time, maxRetries int, state ...api.ActionInvocationState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.deleteByUpdatedAtBeforeAndStates(time, maxRetries, state...)
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -38,22 +40,14 @@ func (s *eventService) createUpdateUpdated(old *Update, new *Update) *Event {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventName api.EventName
|
eventName := api.EventNameUpdateUpdated
|
||||||
|
|
||||||
if old.State == new.State {
|
if old.State != new.State {
|
||||||
eventName = api.EventNameUpdateUpdated
|
eventName = api.EventNameUpdateUpdatedState
|
||||||
} else {
|
|
||||||
switch new.State {
|
|
||||||
case api.UpdateStatePending.Value():
|
|
||||||
eventName = api.EventNameUpdateUpdatedPending
|
|
||||||
break
|
|
||||||
case api.UpdateStateApproved.Value():
|
|
||||||
eventName = api.EventNameUpdateUpdatedApproved
|
|
||||||
break
|
|
||||||
case api.UpdateStateIgnored.Value():
|
|
||||||
eventName = api.EventNameUpdateUpdatedIgnored
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if old.Version != new.Version {
|
||||||
|
eventName = api.EventNameUpdateUpdatedVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
s.createWithWarnOnly(eventName, &api.EventPayloadUpdateUpdatedDto{
|
s.createWithWarnOnly(eventName, &api.EventPayloadUpdateUpdatedDto{
|
||||||
|
@ -80,60 +74,7 @@ func (s *eventService) createUpdateDeleted(e *Update) *Event {
|
||||||
Provider: e.Provider,
|
Provider: e.Provider,
|
||||||
Host: e.Host,
|
Host: e.Host,
|
||||||
Version: e.Version,
|
Version: e.Version,
|
||||||
})
|
State: e.State,
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *eventService) createWebhookCreated(e *Webhook) *Event {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.createWithWarnOnly(api.EventNameWebhookCreated, &api.EventPayloadWebhookCreatedDto{
|
|
||||||
ID: e.ID,
|
|
||||||
Label: e.Label,
|
|
||||||
Type: e.Type,
|
|
||||||
IgnoreHost: e.IgnoreHost,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *eventService) createWebhookUpdated(old *Webhook, new *Webhook) *Event {
|
|
||||||
if old == nil || new == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventName api.EventName
|
|
||||||
|
|
||||||
if old.Label == new.Label {
|
|
||||||
eventName = api.EventNameWebhookUpdatedIgnoreHost
|
|
||||||
} else {
|
|
||||||
eventName = api.EventNameWebhookUpdatedLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
s.createWithWarnOnly(eventName, &api.EventPayloadWebhookUpdatedDto{
|
|
||||||
ID: new.ID,
|
|
||||||
LabelPrior: old.Label,
|
|
||||||
Label: new.Label,
|
|
||||||
IgnoreHostPrior: old.IgnoreHost,
|
|
||||||
IgnoreHost: new.IgnoreHost,
|
|
||||||
Type: new.Type,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *eventService) createWebhookDeleted(e *Webhook) *Event {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.createWithWarnOnly(api.EventNameWebhookDeleted, &api.EventPayloadWebhookDeletedDto{
|
|
||||||
Label: e.Label,
|
|
||||||
Type: e.Type,
|
|
||||||
IgnoreHost: e.IgnoreHost,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -212,3 +153,82 @@ func (s *eventService) windowHasNext(size int, skip int, orderBy string, order s
|
||||||
func (s *eventService) count(state ...api.EventState) (int64, error) {
|
func (s *eventService) count(state ...api.EventState) (int64, error) {
|
||||||
return s.repo.count(state...)
|
return s.repo.count(state...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *eventService) getByState(limit int, state ...api.EventState) ([]*Event, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return nil, errorValidationLimitGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.findAllByState(limit, state...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) updateState(id string, state api.EventState) (*Event, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Event
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.updateState(id, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified event '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) extractPayloadInfo(event *Event) (*eventPayloadInformationDto, error) {
|
||||||
|
if event == nil {
|
||||||
|
return nil, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var bytes []byte
|
||||||
|
|
||||||
|
if bytes, err = event.Payload.MarshalJSON(); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event.Name {
|
||||||
|
case api.EventNameUpdateCreated.Value():
|
||||||
|
var p api.EventPayloadUpdateCreatedDto
|
||||||
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateCreatedDto](bytes); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
|
case api.EventNameUpdateDeleted.Value():
|
||||||
|
var p api.EventPayloadUpdateDeletedDto
|
||||||
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateDeletedDto](bytes); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
|
case api.EventNameUpdateUpdatedState.Value():
|
||||||
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
|
case api.EventNameUpdateUpdatedVersion.Value():
|
||||||
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
|
case api.EventNameUpdateUpdated.Value():
|
||||||
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
|
return nil, newServiceError(general, err)
|
||||||
|
}
|
||||||
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, newServiceError(general, errors.New("no matching event found"))
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,65 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lockService provides methods for locking resources, behavior depends on underlying implementation
|
||||||
type lockService interface {
|
type lockService interface {
|
||||||
init() error
|
// lock locks a resource applying default options (varies for implementations)
|
||||||
tryLock(resource string) error
|
lock(ctx context.Context, resource string) (appLock, error)
|
||||||
release(resource string) error
|
|
||||||
exists(resource string) bool
|
// lockWithOptions locks a resource with given options, not all options are applied (varies for implementations)
|
||||||
stop()
|
lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type appLock interface {
|
||||||
|
// unlock unlocks a lock
|
||||||
|
unlock(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type appLockOption interface {
|
||||||
|
apply(l *appLockOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type appLockOptionFunc func(o *appLockOptions)
|
||||||
|
|
||||||
|
func (f appLockOptionFunc) apply(o *appLockOptions) {
|
||||||
|
f(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
type appLockOptions struct {
|
||||||
|
expiry *time.Duration
|
||||||
|
retryDelay *time.Duration
|
||||||
|
maxRetries *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func withAppLockOptionExpiry(expiry time.Duration) appLockOption {
|
||||||
|
return appLockOptionFunc(func(o *appLockOptions) {
|
||||||
|
o.expiry = &expiry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func withAppLockOptionRetries(retries int) appLockOption {
|
||||||
|
return appLockOptionFunc(func(o *appLockOptions) {
|
||||||
|
o.maxRetries = &retries
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
appLockOptionMaxRetries = math.MaxInt32
|
||||||
|
)
|
||||||
|
|
||||||
|
func withAppLockOptionInfiniteRetries() appLockOption {
|
||||||
|
return appLockOptionFunc(func(o *appLockOptions) {
|
||||||
|
o.maxRetries = &appLockOptionMaxRetries
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func withAppLockOptionRetryDelay(retryDelay time.Duration) appLockOption {
|
||||||
|
return appLockOptionFunc(func(o *appLockOptions) {
|
||||||
|
o.retryDelay = &retryDelay
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,75 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"git.myservermanager.com/varakh/upda/util"
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lockMemService struct {
|
type lockMemService struct {
|
||||||
registry *util.InMemoryLockRegistry
|
registry *util.InMemoryLockRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errLockMemNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||||
|
)
|
||||||
|
|
||||||
func newLockMemService() lockService {
|
func newLockMemService() lockService {
|
||||||
|
zap.L().Info("Initializing in-memory locking service")
|
||||||
return &lockMemService{registry: util.NewInMemoryLockRegistry()}
|
return &lockMemService{registry: util.NewInMemoryLockRegistry()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *lockMemService) init() error {
|
// lock locks a given resource without any options (default expiration)
|
||||||
zap.L().Info("Initialized in-memory locking service")
|
func (s *lockMemService) lock(ctx context.Context, resource string) (appLock, error) {
|
||||||
return nil
|
return s.lockWithOptions(ctx, resource, withAppLockOptionExpiry(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *lockMemService) tryLock(resource string) error {
|
// lockWithOptions locks a given resource, only TTL as option is supported
|
||||||
|
func (s *lockMemService) lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error) {
|
||||||
if resource == "" {
|
if resource == "" {
|
||||||
return errorValidationNotBlank
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration time.Duration = 0
|
||||||
|
if options != nil {
|
||||||
|
lockOptions := &appLockOptions{}
|
||||||
|
for _, o := range options {
|
||||||
|
o.apply(lockOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lockOptions.expiry != nil {
|
||||||
|
expiration = *lockOptions.expiry
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Sugar().Debugf("Trying to lock '%s'", resource)
|
zap.L().Sugar().Debugf("Trying to lock '%s'", resource)
|
||||||
s.registry.Lock(resource)
|
|
||||||
|
s.registry.LockWithTTL(resource, expiration)
|
||||||
|
|
||||||
zap.L().Sugar().Debugf("Locked '%s'", resource)
|
zap.L().Sugar().Debugf("Locked '%s'", resource)
|
||||||
|
|
||||||
|
l := &inMemoryLock{
|
||||||
|
registry: s.registry,
|
||||||
|
resource: resource,
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ appLock = (*inMemoryLock)(nil)
|
||||||
|
|
||||||
|
type inMemoryLock struct {
|
||||||
|
registry *util.InMemoryLockRegistry
|
||||||
|
resource string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r inMemoryLock) unlock(ctx context.Context) error {
|
||||||
|
zap.L().Sugar().Debugf("Unlocking '%s'", r.resource)
|
||||||
|
|
||||||
|
if err := r.registry.Unlock(r.resource); err != nil {
|
||||||
|
return errLockMemNotReleased
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *lockMemService) release(resource string) error {
|
|
||||||
if resource == "" {
|
|
||||||
return errorValidationNotBlank
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.L().Sugar().Debugf("Releasing lock '%s'", resource)
|
|
||||||
err := s.registry.Unlock(resource)
|
|
||||||
zap.L().Sugar().Debugf("Released lock '%s'", resource)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *lockMemService) exists(resource string) bool {
|
|
||||||
return s.registry.Exists(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *lockMemService) stop() {
|
|
||||||
zap.L().Info("Clearing in-memory locking service")
|
|
||||||
s.registry.Clear()
|
|
||||||
}
|
|
||||||
|
|
29
server/service_lock_mem_test.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testLockName = "test_lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLockExpiresAndCannotBeReleased(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
s := newLockMemService()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
lock, lockErr := s.lockWithOptions(ctx, testLockName, withAppLockOptionExpiry(250*time.Millisecond))
|
||||||
|
a.Nil(lockErr)
|
||||||
|
a.NotNil(lock)
|
||||||
|
|
||||||
|
time.Sleep(251 * time.Millisecond)
|
||||||
|
|
||||||
|
unlockErr := lock.unlock(ctx)
|
||||||
|
a.NotNil(unlockErr)
|
||||||
|
a.ErrorContains(unlockErr, "could not release lock")
|
||||||
|
}
|
109
server/service_lock_redis.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-redsync/redsync/v4"
|
||||||
|
redsyncgoredis "github.com/go-redsync/redsync/v4/redis/goredis/v9"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lockRedisService struct {
|
||||||
|
rs *redsync.Redsync
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errLockRedisNotObtained = newServiceError(conflict, errors.New("lock service: could not obtain lock"))
|
||||||
|
errLockRedisNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLockRedisService(lc *lockConfig) (lockService, error) {
|
||||||
|
zap.L().Info("Initializing REDIS locking service")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var redisOptions *redis.Options
|
||||||
|
redisOptions, err = redis.ParseURL(lc.redisUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("lock service: cannot parse REDIS URL '%s' to set up locking: %s", lc.redisUrl, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := redis.NewClient(redisOptions)
|
||||||
|
if err = c.Ping(context.Background()).Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("lock service: failed to connect to REDIS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := redsyncgoredis.NewPool(c)
|
||||||
|
rs := redsync.New(pool)
|
||||||
|
|
||||||
|
return &lockRedisService{rs: rs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock locks a given resource without any options
|
||||||
|
func (s *lockRedisService) lock(ctx context.Context, resource string) (appLock, error) {
|
||||||
|
return s.lockWithOptions(ctx, resource, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lockWithOptions locks a given resource considering all options
|
||||||
|
func (s *lockRedisService) lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error) {
|
||||||
|
if resource == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var rsOptions []redsync.Option
|
||||||
|
|
||||||
|
if options != nil {
|
||||||
|
lockOptions := &appLockOptions{}
|
||||||
|
for _, o := range options {
|
||||||
|
o.apply(lockOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if lockOptions.expiry != nil {
|
||||||
|
rsOptions = append(rsOptions, redsync.WithExpiry(*lockOptions.expiry))
|
||||||
|
}
|
||||||
|
if lockOptions.maxRetries != nil {
|
||||||
|
rsOptions = append(rsOptions, redsync.WithTries(*lockOptions.maxRetries))
|
||||||
|
}
|
||||||
|
if lockOptions.retryDelay != nil {
|
||||||
|
rsOptions = append(rsOptions, redsync.WithRetryDelay(*lockOptions.retryDelay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mu := s.rs.NewMutex(resource, rsOptions...)
|
||||||
|
|
||||||
|
zap.L().Sugar().Debugf("Trying to lock '%s'", resource)
|
||||||
|
|
||||||
|
if err := mu.LockContext(ctx); err != nil {
|
||||||
|
return nil, errLockRedisNotObtained
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Debugf("Locked '%s'", resource)
|
||||||
|
|
||||||
|
l := &redisLock{
|
||||||
|
mu: mu,
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ appLock = (*redisLock)(nil)
|
||||||
|
|
||||||
|
type redisLock struct {
|
||||||
|
mu *redsync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redisLock) unlock(ctx context.Context) error {
|
||||||
|
zap.L().Sugar().Debugf("Unlocking '%s'", r.mu.Name())
|
||||||
|
|
||||||
|
unlocked, err := r.mu.UnlockContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errLockRedisNotReleased
|
||||||
|
}
|
||||||
|
if !unlocked {
|
||||||
|
return errLockRedisNotReleased
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package server
|
||||||
import (
|
import (
|
||||||
"github.com/Depado/ginprom"
|
"github.com/Depado/ginprom"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type prometheusService struct {
|
type prometheusService struct {
|
||||||
|
@ -19,7 +18,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
||||||
if c.secureTokenEnabled {
|
if c.secureTokenEnabled {
|
||||||
p = ginprom.New(
|
p = ginprom.New(
|
||||||
ginprom.Engine(r),
|
ginprom.Engine(r),
|
||||||
ginprom.Namespace(Name),
|
ginprom.Namespace(name),
|
||||||
ginprom.Subsystem(""),
|
ginprom.Subsystem(""),
|
||||||
ginprom.Path(c.path),
|
ginprom.Path(c.path),
|
||||||
ginprom.Ignore(c.path),
|
ginprom.Ignore(c.path),
|
||||||
|
@ -28,7 +27,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
||||||
} else {
|
} else {
|
||||||
p = ginprom.New(
|
p = ginprom.New(
|
||||||
ginprom.Engine(r),
|
ginprom.Engine(r),
|
||||||
ginprom.Namespace(Name),
|
ginprom.Namespace(name),
|
||||||
ginprom.Subsystem(""),
|
ginprom.Subsystem(""),
|
||||||
ginprom.Ignore(c.path),
|
ginprom.Ignore(c.path),
|
||||||
ginprom.Path(c.path),
|
ginprom.Path(c.path),
|
||||||
|
@ -42,25 +41,34 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *prometheusService) init() {
|
func (s *prometheusService) init() error {
|
||||||
if !s.config.enabled {
|
if !s.config.enabled {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
if err := s.registerGaugeNoLabels(metricUpdatesTotal, metricUpdatesTotalHelp); err != nil {
|
||||||
|
return err
|
||||||
err = s.registerGaugeNoLabels(metricUpdatesTotal, metricUpdatesTotalHelp)
|
|
||||||
err = s.registerGaugeNoLabels(metricUpdatesPending, metricUpdatesPendingHelp)
|
|
||||||
err = s.registerGaugeNoLabels(metricUpdatesIgnored, metricUpdatesIgnoredHelp)
|
|
||||||
err = s.registerGaugeNoLabels(metricUpdatesApproved, metricUpdatesApprovedHelp)
|
|
||||||
err = s.registerGauge(metricUpdates, metricUpdatesHelp, []string{"application", "provider", "host"})
|
|
||||||
|
|
||||||
err = s.registerGaugeNoLabels(metricWebhooks, metricWebhooksHelp)
|
|
||||||
err = s.registerGaugeNoLabels(metricEvents, metricEventsHelp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Fatalf("Cannot initialize service. Reason: %v", err)
|
|
||||||
}
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricUpdatesPending, metricUpdatesPendingHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricUpdatesIgnored, metricUpdatesIgnoredHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricUpdatesApproved, metricUpdatesApprovedHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricWebhooks, metricWebhooksHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricEvents, metricEventsHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.registerGaugeNoLabels(metricActions, metricActionsHelp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *prometheusService) registerGaugeNoLabels(name string, help string) error {
|
func (s *prometheusService) registerGaugeNoLabels(name string, help string) error {
|
||||||
|
|
108
server/service_secret.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type secretService struct {
|
||||||
|
repo secretRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecretService(r secretRepository) *secretService {
|
||||||
|
return &secretService{
|
||||||
|
repo: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) get(id string) (*Secret, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.findById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) getValueByKey(key string) (string, error) {
|
||||||
|
if key == "" {
|
||||||
|
return "", errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Secret
|
||||||
|
|
||||||
|
if e, err = s.repo.findByKey(key); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) getAll() ([]*Secret, error) {
|
||||||
|
return s.repo.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) upsert(key string, value string) (*Secret, error) {
|
||||||
|
if key == "" || value == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Secret
|
||||||
|
var err error
|
||||||
|
|
||||||
|
e, err = s.repo.findByKey(key)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errorResourceNotFound) {
|
||||||
|
return nil, err
|
||||||
|
} else if err != nil && errors.Is(err, errorResourceNotFound) {
|
||||||
|
if e, err = s.repo.create(key, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
zap.L().Sugar().Infof("Created secret '%s'", e.Key)
|
||||||
|
} else {
|
||||||
|
if e, err = s.repo.update(e.ID.String(), value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
zap.L().Sugar().Infof("Updated secret '%s'", e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) updateValue(id string, value string) (*Secret, error) {
|
||||||
|
if id == "" || value == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Secret
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.update(id, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified secret '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secretService) delete(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if _, err = s.get(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.repo.delete(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Deleted secret '%v'", id)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
"github.com/go-co-op/gocron"
|
redislock "github.com/go-co-op/gocron-redis-lock/v2"
|
||||||
redislock "github.com/go-co-op/gocron-redis-lock"
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"time"
|
"time"
|
||||||
|
@ -12,6 +14,8 @@ import (
|
||||||
type taskService struct {
|
type taskService struct {
|
||||||
updateService *updateService
|
updateService *updateService
|
||||||
eventService *eventService
|
eventService *eventService
|
||||||
|
actionService *actionService
|
||||||
|
actionInvocationService *actionInvocationService
|
||||||
webhookService *webhookService
|
webhookService *webhookService
|
||||||
lockService lockService
|
lockService lockService
|
||||||
prometheusService *prometheusService
|
prometheusService *prometheusService
|
||||||
|
@ -19,46 +23,67 @@ type taskService struct {
|
||||||
taskConfig *taskConfig
|
taskConfig *taskConfig
|
||||||
lockConfig *lockConfig
|
lockConfig *lockConfig
|
||||||
prometheusConfig *prometheusConfig
|
prometheusConfig *prometheusConfig
|
||||||
scheduler *gocron.Scheduler
|
scheduler gocron.Scheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
taskLockNameUpdatesCleanStale = "updates_clean_stale"
|
jobUpdatesCleanStale = "UPDATES_CLEAN_STALE"
|
||||||
taskLockNameEventsCleanStale = "events_clean_stale"
|
jobEventsCleanStale = "EVENTS_CLEAN_STALE"
|
||||||
taskLockNamePrometheusUpdate = "prometheus_update"
|
jobActionsEnqueue = "ACTIONS_ENQUEUE"
|
||||||
|
jobActionsInvoke = "ACTIONS_INVOKE"
|
||||||
|
jobActionsCleanStale = "ACTIONS_CLEAN_STALE"
|
||||||
|
jobPrometheusRefresh = "PROMETHEUS_REFRESH"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTaskService(u *updateService, e *eventService, w *webhookService, l lockService, p *prometheusService, ac *appConfig, tc *taskConfig, lc *lockConfig, pc *prometheusConfig) *taskService {
|
var (
|
||||||
location, err := time.LoadLocation(ac.timeZone)
|
initialTasksStartDelay = time.Now().Add(10 * time.Second)
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
func newTaskService(u *updateService, e *eventService, w *webhookService, a *actionService, ai *actionInvocationService, l lockService, p *prometheusService, ac *appConfig, tc *taskConfig, lc *lockConfig, pc *prometheusConfig) (*taskService, error) {
|
||||||
zap.L().Sugar().Fatalf("Could not initialize correct timezone for scheduler. Reason: %s", err.Error())
|
var err error
|
||||||
|
var location *time.Location
|
||||||
|
if location, err = time.LoadLocation(ac.timeZone); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not initialize correct timezone for scheduler: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gocron.SetPanicHandler(func(jobName string, value any) {
|
// global job options
|
||||||
zap.L().Sugar().Errorf("Job '%s' had a panic %v", jobName, value)
|
singletonModeOption := gocron.WithSingletonMode(gocron.LimitModeReschedule)
|
||||||
|
errorEventListener := gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
|
||||||
|
zap.L().Sugar().Errorf("Job '%s' (%v) had a panic %v", jobName, jobID, err)
|
||||||
})
|
})
|
||||||
|
successEventListener := gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
|
||||||
|
zap.L().Sugar().Debugf("Job '%s' (%v) finished", jobName, jobID)
|
||||||
|
})
|
||||||
|
eventListenerOption := gocron.WithEventListeners(successEventListener, errorEventListener)
|
||||||
|
startAtOption := gocron.WithStartAt(gocron.WithStartDateTime(initialTasksStartDelay))
|
||||||
|
|
||||||
scheduler := gocron.NewScheduler(location)
|
// global scheduler options
|
||||||
|
schedulerOptions := []gocron.SchedulerOption{gocron.WithLocation(location), gocron.WithGlobalJobOptions(singletonModeOption, eventListenerOption, startAtOption)}
|
||||||
|
|
||||||
if lc.redisEnabled {
|
if lc.redisEnabled {
|
||||||
var redisOptions *redis.Options
|
var redisOptions *redis.Options
|
||||||
redisOptions, err = redis.ParseURL(lc.redisUrl)
|
redisOptions, err = redis.ParseURL(lc.redisUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Sugar().Fatalf("Cannot parse REDIS URL '%s' to set up locking. Reason: %s", lc.redisUrl, err.Error())
|
return nil, fmt.Errorf("cannot parse REDIS URL '%s' to set up locking for scheduler: %s", lc.redisUrl, err)
|
||||||
}
|
}
|
||||||
redisClient := redis.NewClient(redisOptions)
|
redisClient := redis.NewClient(redisOptions)
|
||||||
locker, err := redislock.NewRedisLocker(redisClient, redislock.WithTries(1))
|
|
||||||
if err != nil {
|
var locker gocron.Locker
|
||||||
zap.L().Sugar().Fatalf("Cannot set up REDIS locker. Reason: %s", err.Error())
|
if locker, err = redislock.NewRedisLocker(redisClient, redislock.WithTries(1), redislock.WithExpiry(30*time.Second), redislock.WithRetryDelay(5*time.Second)); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot set up REDIS locker for scheduler: %s", err)
|
||||||
}
|
}
|
||||||
scheduler.WithDistributedLocker(locker)
|
|
||||||
|
schedulerOptions = append(schedulerOptions, gocron.WithDistributedLocker(locker))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduler, _ := gocron.NewScheduler(schedulerOptions...)
|
||||||
|
|
||||||
return &taskService{
|
return &taskService{
|
||||||
updateService: u,
|
updateService: u,
|
||||||
eventService: e,
|
eventService: e,
|
||||||
|
actionService: a,
|
||||||
|
actionInvocationService: ai,
|
||||||
webhookService: w,
|
webhookService: w,
|
||||||
lockService: l,
|
lockService: l,
|
||||||
prometheusService: p,
|
prometheusService: p,
|
||||||
|
@ -67,52 +92,54 @@ func newTaskService(u *updateService, e *eventService, w *webhookService, l lock
|
||||||
lockConfig: lc,
|
lockConfig: lc,
|
||||||
prometheusConfig: pc,
|
prometheusConfig: pc,
|
||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) init() {
|
func (s *taskService) init() error {
|
||||||
s.configureCleanupStaleUpdatesTask()
|
if err := s.configureCleanupStaleUpdatesTask(); err != nil {
|
||||||
s.configureCleanupStaleEventsTask()
|
return err
|
||||||
s.configurePrometheusRefreshTask()
|
}
|
||||||
|
if err := s.configureCleanupStaleEventsTask(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.configureActionsEnqueueTask(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.configureActionsInvokeTask(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.configureCleanupStaleActionsTask(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.configurePrometheusRefreshTask(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) stop() {
|
func (s *taskService) stop() {
|
||||||
zap.L().Sugar().Infof("Stopping %d periodic tasks...", len(s.scheduler.Jobs()))
|
zap.L().Sugar().Infof("Stopping %d periodic tasks...", len(s.scheduler.Jobs()))
|
||||||
s.scheduler.Stop()
|
if err := s.scheduler.StopJobs(); err != nil {
|
||||||
s.lockService.stop()
|
zap.L().Sugar().Warnf("Cannot stop periodic tasks. Reason: %v", err)
|
||||||
|
}
|
||||||
|
if err := s.scheduler.Shutdown(); err != nil {
|
||||||
|
zap.L().Sugar().Warnf("Cannot shut down scheduler. Reason: %v", err)
|
||||||
|
}
|
||||||
zap.L().Info("Stopped all periodic tasks")
|
zap.L().Info("Stopped all periodic tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) start() {
|
func (s *taskService) start() {
|
||||||
s.scheduler.StartAsync()
|
s.scheduler.Start()
|
||||||
zap.L().Sugar().Infof("Started %d periodic tasks", len(s.scheduler.Jobs()))
|
zap.L().Sugar().Infof("Started %d periodic tasks", len(s.scheduler.Jobs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) configureCleanupStaleUpdatesTask() {
|
func (s *taskService) configureCleanupStaleUpdatesTask() error {
|
||||||
if !s.taskConfig.updateCleanStaleEnabled {
|
if !s.taskConfig.updateCleanStaleEnabled {
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
initialDelay := time.Now().Add(10 * time.Second)
|
|
||||||
_, err := s.scheduler.Every(s.taskConfig.updateCleanStaleInterval).
|
|
||||||
StartAt(initialDelay).
|
|
||||||
Do(func() {
|
|
||||||
resource := taskLockNameUpdatesCleanStale
|
|
||||||
// distributed lock handled via gocron-redis-lock for tasks
|
|
||||||
if !s.lockConfig.redisEnabled {
|
|
||||||
// skip execution if lock already exists, wait otherwise
|
|
||||||
if lockExists := s.lockService.exists(resource); lockExists {
|
|
||||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = s.lockService.tryLock(resource)
|
|
||||||
defer func(lockService lockService, resource string) {
|
|
||||||
err := lockService.release(resource)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
|
||||||
}
|
|
||||||
}(s.lockService, resource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runnable := func() {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
t = t.Add(-s.taskConfig.updateCleanStaleMaxAge)
|
t = t.Add(-s.taskConfig.updateCleanStaleMaxAge)
|
||||||
|
|
||||||
|
@ -127,48 +154,31 @@ func (s *taskService) configureCleanupStaleUpdatesTask() {
|
||||||
if c > 0 {
|
if c > 0 {
|
||||||
zap.L().Sugar().Infof("Cleaned up '%d' stale updates", c)
|
zap.L().Sugar().Infof("Cleaned up '%d' stale updates", c)
|
||||||
} else {
|
} else {
|
||||||
zap.L().Info("No stale updates found to clean up")
|
zap.L().Debug("No stale updates found to clean up")
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Fatalf("Could not create task for cleaning stale updates. Reason: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) configureCleanupStaleEventsTask() {
|
scheduledJob := gocron.DurationJob(s.taskConfig.updateCleanStaleInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobUpdatesCleanStale)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for cleaning stale updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureCleanupStaleEventsTask() error {
|
||||||
if !s.taskConfig.eventCleanStaleEnabled {
|
if !s.taskConfig.eventCleanStaleEnabled {
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
initialDelay := time.Now().Add(5 * time.Second)
|
|
||||||
_, err := s.scheduler.Every(s.taskConfig.eventCleanStaleInterval).
|
|
||||||
StartAt(initialDelay).
|
|
||||||
Do(func() {
|
|
||||||
resource := taskLockNameEventsCleanStale
|
|
||||||
// distributed lock handled via gocron-redis-lock for tasks
|
|
||||||
if !s.lockConfig.redisEnabled {
|
|
||||||
// skip execution if lock already exists, wait otherwise
|
|
||||||
if lockExists := s.lockService.exists(resource); lockExists {
|
|
||||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = s.lockService.tryLock(resource)
|
|
||||||
defer func(lockService lockService, resource string) {
|
|
||||||
err := lockService.release(resource)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
|
||||||
}
|
|
||||||
}(s.lockService, resource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runnable := func() {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
t = t.Add(-s.taskConfig.eventCleanStaleMaxAge)
|
t = t.Add(-s.taskConfig.eventCleanStaleMaxAge)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var c int64
|
var c int64
|
||||||
|
|
||||||
if c, err = s.eventService.cleanStale(t, api.EventStateCreated); err != nil {
|
if c, err = s.eventService.cleanStale(t, api.EventStateCreated, api.EventStateEnqueued); err != nil {
|
||||||
zap.L().Sugar().Errorf("Could not clean up stale events older than %s (%s). Reason: %s", s.taskConfig.eventCleanStaleMaxAge, t, err.Error())
|
zap.L().Sugar().Errorf("Could not clean up stale events older than %s (%s). Reason: %s", s.taskConfig.eventCleanStaleMaxAge, t, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -176,42 +186,101 @@ func (s *taskService) configureCleanupStaleEventsTask() {
|
||||||
if c > 0 {
|
if c > 0 {
|
||||||
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
|
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
|
||||||
} else {
|
} else {
|
||||||
zap.L().Info("No stale events found to clean up")
|
zap.L().Debug("No stale events found to clean up")
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Fatalf("Could not create task for cleaning stale events. Reason: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *taskService) configurePrometheusRefreshTask() {
|
scheduledJob := gocron.DurationJob(s.taskConfig.eventCleanStaleInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobEventsCleanStale)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for cleaning stale events: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureActionsEnqueueTask() error {
|
||||||
|
if !s.taskConfig.actionsEnqueueEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runnable := func() {
|
||||||
|
if err := s.actionInvocationService.enqueue(s.taskConfig.actionsEnqueueBatchSize); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could enqueue actions. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledJob := gocron.DurationJob(s.taskConfig.actionsEnqueueInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsEnqueue)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for enqueueing actions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureActionsInvokeTask() error {
|
||||||
|
if !s.taskConfig.actionsInvokeEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runnable := func() {
|
||||||
|
if err := s.actionInvocationService.invoke(s.taskConfig.actionsInvokeBatchSize, s.taskConfig.actionsInvokeMaxRetries); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could invoke actions. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledJob := gocron.DurationJob(s.taskConfig.actionsInvokeInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsInvoke)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for invoking actions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureCleanupStaleActionsTask() error {
|
||||||
|
if !s.taskConfig.actionsCleanStaleEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runnable := func() {
|
||||||
|
t := time.Now()
|
||||||
|
t = t.Add(-s.taskConfig.actionsCleanStaleMaxAge)
|
||||||
|
|
||||||
|
var cError int64
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if cError, err = s.actionInvocationService.cleanStale(t, s.taskConfig.actionsInvokeMaxRetries, api.ActionInvocationStateError); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not clean up error stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cSuccess int64
|
||||||
|
if cSuccess, err = s.actionInvocationService.cleanStale(t, 0, api.ActionInvocationStateSuccess); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not clean up success stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c := cError + cSuccess
|
||||||
|
if c > 0 {
|
||||||
|
zap.L().Sugar().Infof("Cleaned up '%d' stale actions", c)
|
||||||
|
} else {
|
||||||
|
zap.L().Debug("No stale actions found to clean up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledJob := gocron.DurationJob(s.taskConfig.actionsCleanStaleInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsCleanStale)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for cleaning stale actions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configurePrometheusRefreshTask() error {
|
||||||
if !s.prometheusConfig.enabled {
|
if !s.prometheusConfig.enabled {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
initialDelay := time.Now().Add(10 * time.Second)
|
runnable := func() {
|
||||||
_, err := s.scheduler.Every(s.taskConfig.prometheusRefreshInterval).
|
|
||||||
StartAt(initialDelay).
|
|
||||||
Do(func() {
|
|
||||||
resource := taskLockNamePrometheusUpdate
|
|
||||||
// distributed lock handled via gocron-redis-lock for tasks
|
|
||||||
if !s.lockConfig.redisEnabled {
|
|
||||||
// skip execution if lock already exists, wait otherwise
|
|
||||||
if lockExists := s.lockService.exists(resource); lockExists {
|
|
||||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = s.lockService.tryLock(resource)
|
|
||||||
defer func(lockService lockService, resource string) {
|
|
||||||
err := lockService.release(resource)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
|
||||||
}
|
|
||||||
}(s.lockService, resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates with labels and collect stats about state
|
|
||||||
updates, updatesError := s.updateService.getAll()
|
updates, updatesError := s.updateService.getAll()
|
||||||
|
|
||||||
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesTotal, float64(len(updates))); updatesError != nil {
|
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesTotal, float64(len(updates))); updatesError != nil {
|
||||||
|
@ -223,20 +292,12 @@ func (s *taskService) configurePrometheusRefreshTask() {
|
||||||
var ackTotal int64
|
var ackTotal int64
|
||||||
|
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
var updateState float64
|
|
||||||
if api.UpdateStatePending.Value() == update.State {
|
if api.UpdateStatePending.Value() == update.State {
|
||||||
pendingTotal += 1
|
pendingTotal += 1
|
||||||
updateState = 0
|
|
||||||
} else if api.UpdateStateIgnored.Value() == update.State {
|
} else if api.UpdateStateIgnored.Value() == update.State {
|
||||||
ignoredTotal += 1
|
ignoredTotal += 1
|
||||||
updateState = 2
|
|
||||||
} else if api.UpdateStateApproved.Value() == update.State {
|
} else if api.UpdateStateApproved.Value() == update.State {
|
||||||
ackTotal += 1
|
ackTotal += 1
|
||||||
updateState = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if updatesError = s.prometheusService.setGauge(metricUpdates, []string{update.Application, update.Provider, update.Host}, updateState); updatesError != nil {
|
|
||||||
zap.L().Sugar().Errorf("Could not refresh updates prometheus metric. Reason: %s", updatesError.Error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +311,6 @@ func (s *taskService) configurePrometheusRefreshTask() {
|
||||||
zap.L().Sugar().Errorf("Could not refresh updates approved prometheus metric. Reason: %s", updatesError.Error())
|
zap.L().Sugar().Errorf("Could not refresh updates approved prometheus metric. Reason: %s", updatesError.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// webhooks
|
|
||||||
var webhooksTotal int64
|
var webhooksTotal int64
|
||||||
var webhooksError error
|
var webhooksError error
|
||||||
webhooksTotal, webhooksError = s.webhookService.count()
|
webhooksTotal, webhooksError = s.webhookService.count()
|
||||||
|
@ -258,16 +318,25 @@ func (s *taskService) configurePrometheusRefreshTask() {
|
||||||
zap.L().Sugar().Errorf("Could not refresh webhooks prometheus metric. Reason: %s", webhooksError.Error())
|
zap.L().Sugar().Errorf("Could not refresh webhooks prometheus metric. Reason: %s", webhooksError.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// events
|
|
||||||
var eventsTotal int64
|
var eventsTotal int64
|
||||||
var eventsError error
|
var eventsError error
|
||||||
eventsTotal, eventsError = s.eventService.count()
|
eventsTotal, eventsError = s.eventService.count()
|
||||||
if eventsError = s.prometheusService.setGaugeNoLabels(metricEvents, float64(eventsTotal)); eventsError != nil {
|
if eventsError = s.prometheusService.setGaugeNoLabels(metricEvents, float64(eventsTotal)); eventsError != nil {
|
||||||
zap.L().Sugar().Errorf("Could not refresh events prometheus metric. Reason: %s", eventsError.Error())
|
zap.L().Sugar().Errorf("Could not refresh events prometheus metric. Reason: %s", eventsError.Error())
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
var actionsTotal int64
|
||||||
zap.L().Sugar().Fatalf("Could not create task for refreshing prometheus. Reason: %s", err.Error())
|
var actionsError error
|
||||||
|
actionsTotal, actionsError = s.actionService.count()
|
||||||
|
if actionsError = s.prometheusService.setGaugeNoLabels(metricActions, float64(actionsTotal)); actionsError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh actions prometheus metric. Reason: %s", actionsError.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduledJob := gocron.DurationJob(s.taskConfig.prometheusRefreshInterval)
|
||||||
|
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobPrometheusRefresh)); err != nil {
|
||||||
|
return fmt.Errorf("could not create task for refreshing prometheus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -10,14 +10,12 @@ import (
|
||||||
type updateService struct {
|
type updateService struct {
|
||||||
repo updateRepository
|
repo updateRepository
|
||||||
eventService *eventService
|
eventService *eventService
|
||||||
prometheusService *prometheusService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUpdateService(r updateRepository, e *eventService, p *prometheusService) *updateService {
|
func newUpdateService(r updateRepository, e *eventService) *updateService {
|
||||||
return &updateService{
|
return &updateService{
|
||||||
repo: r,
|
repo: r,
|
||||||
eventService: e,
|
eventService: e,
|
||||||
prometheusService: p,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,10 +116,6 @@ func (s *updateService) delete(id string) error {
|
||||||
|
|
||||||
s.eventService.createUpdateDeleted(e)
|
s.eventService.createUpdateDeleted(e)
|
||||||
|
|
||||||
if err = s.prometheusService.setGauge(metricUpdates, []string{e.Application, e.Provider, e.Host}, -1); err != nil {
|
|
||||||
zap.L().Sugar().Errorf("Could not refresh updates prometheus metric for deleted update '%v'. Reason: %v", e.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.L().Sugar().Infof("Deleted update '%v'", id)
|
zap.L().Sugar().Infof("Deleted update '%v'", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type webhookService struct {
|
type webhookService struct {
|
||||||
repo WebhookRepository
|
repo webhookRepository
|
||||||
webhookConfig *webhookConfig
|
webhookConfig *webhookConfig
|
||||||
eventService *eventService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebhookService(r WebhookRepository, c *webhookConfig, e *eventService) *webhookService {
|
func newWebhookService(r webhookRepository, c *webhookConfig) *webhookService {
|
||||||
return &webhookService{
|
return &webhookService{
|
||||||
repo: r,
|
repo: r,
|
||||||
webhookConfig: c,
|
webhookConfig: c,
|
||||||
eventService: e,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,16 +40,15 @@ func (s *webhookService) create(label string, t api.WebhookType, ignoreHost bool
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var token string
|
var token string
|
||||||
var e *Webhook
|
|
||||||
|
|
||||||
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
|
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
|
||||||
return nil, newServiceError(General, fmt.Errorf("token generation failed: %w", err))
|
return nil, newServiceError(general, fmt.Errorf("token generation failed: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var e *Webhook
|
||||||
if e, err = s.repo.create(label, t, token, ignoreHost); err != nil {
|
if e, err = s.repo.create(label, t, token, ignoreHost); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
s.eventService.createWebhookCreated(e)
|
|
||||||
zap.L().Sugar().Info("Created webhook")
|
zap.L().Sugar().Info("Created webhook")
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
@ -69,12 +66,10 @@ func (s *webhookService) updateLabel(id string, label string) (*Webhook, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
old := e
|
|
||||||
if e, err = s.repo.updateLabel(id, label); err != nil {
|
if e, err = s.repo.updateLabel(id, label); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.eventService.createWebhookUpdated(old, e)
|
|
||||||
zap.L().Sugar().Infof("Modified webhook '%v'", id)
|
zap.L().Sugar().Infof("Modified webhook '%v'", id)
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
@ -91,12 +86,10 @@ func (s *webhookService) updateIgnoreHost(id string, ignoreHost bool) (*Webhook,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
old := e
|
|
||||||
if e, err = s.repo.updateIgnoreHost(id, ignoreHost); err != nil {
|
if e, err = s.repo.updateIgnoreHost(id, ignoreHost); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.eventService.createWebhookUpdated(old, e)
|
|
||||||
zap.L().Sugar().Infof("Modified webhook '%v'", id)
|
zap.L().Sugar().Infof("Modified webhook '%v'", id)
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
@ -115,7 +108,6 @@ func (s *webhookService) delete(id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.eventService.createWebhookDeleted(e)
|
|
||||||
zap.L().Sugar().Infof("Deleted webhook '%v'", id)
|
zap.L().Sugar().Infof("Deleted webhook '%v'", id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
2
server/web/.env
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_API_URL=
|
||||||
|
VITE_APP_TITLE=upda
|
2
server/web/.env.development
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_API_URL=http://localhost:8080/api/v1/
|
||||||
|
VITE_APP_TITLE='upda dev'
|
1
server/web/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_doc/*.png filter=lfs diff=lfs merge=lfs -text
|
32
server/web/.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
/build*
|
||||||
|
.run/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
ci/*
|
||||||
|
!ci/.gitkeep
|
||||||
|
|
||||||
|
# production
|
||||||
|
build/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.idea
|
||||||
|
/hs_err_pid14224.log
|
2
server/web/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
engine-strict=true
|
||||||
|
legacy-peer-deps=true
|
2
server/web/.nvmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
lts/iron
|
||||||
|
v20
|
7
server/web/.prettierignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules
|
||||||
|
storybook-static
|
||||||
|
package-lock.json
|
||||||
|
dist
|
||||||
|
ci
|
||||||
|
build
|
||||||
|
public
|
9
server/web/.prettierrc
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"singleQuote": true,
|
||||||
|
"useTabs": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSameLine": true
|
||||||
|
}
|
41
server/web/.stylelintrc
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"extends": ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||||
|
"plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties", "stylelint-prettier"],
|
||||||
|
"rules": {
|
||||||
|
"comment-empty-line-before": null,
|
||||||
|
"selector-class-pattern": null,
|
||||||
|
"function-name-case": [
|
||||||
|
"lower",
|
||||||
|
{
|
||||||
|
"ignoreFunctions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-invalid-double-slash-comments": null,
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
"declaration-empty-line-before": null,
|
||||||
|
"selector-pseudo-element-colon-notation": "single",
|
||||||
|
"selector-pseudo-class-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignorePseudoClasses": ["global"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prettier/prettier": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.less"
|
||||||
|
],
|
||||||
|
"customSyntax": "postcss-less"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignoreFiles": [
|
||||||
|
"**/*.json",
|
||||||
|
"**/*.svg",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.js",
|
||||||
|
"node_modules/"
|
||||||
|
]
|
||||||
|
}
|
75
server/web/README.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# upda-ui
|
||||||
|
|
||||||
|
Frontend for upda - **Up**date **Da**shboard in React, TypeScript, Redux.
|
||||||
|
|
||||||
|
The main git repository is hosted at
|
||||||
|
_[https://git.myservermanager.com/varakh/upda-ui](https://git.myservermanager.com/varakh/upda-ui)_. Other repositories
|
||||||
|
are mirrors and pull requests, issues, and planning are managed there.
|
||||||
|
|
||||||
|
Contributions are very welcome!
|
||||||
|
|
||||||
|
[Official documentation](https://git.myservermanager.com/varakh/upda-docs) is hosted in a separate git repository.
|
||||||
|
|
||||||
|
## Development & contribution
|
||||||
|
|
||||||
|
Contributions are very welcome!
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
It's probably worth checking out a node environment manager like [nvm manager](https://github.com/nvm-sh/nvm).
|
||||||
|
|
||||||
|
Required node and npm versions are outlined in the `package.json`.
|
||||||
|
|
||||||
|
### Setup instructions
|
||||||
|
|
||||||
|
Run `npm install` which should install all dependencies.
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
Use the `npm run start` command to start the development setup. Backend should be running.
|
||||||
|
|
||||||
|
### Configuration magic in docker
|
||||||
|
|
||||||
|
What about configuration? How does the pre-compiled set of html, js and css files know about the environment variables?
|
||||||
|
|
||||||
|
In contrast to manual build, the docker image allows dynamic override of configuration, but only those outlined in
|
||||||
|
the `.env` file.
|
||||||
|
|
||||||
|
In production, all configuration values are dynamically generated inside the Docker image with a helper script
|
||||||
|
called `docker-env.sh`:
|
||||||
|
|
||||||
|
1. During docker build a template `.env` file and the helper script are copied to the docker image
|
||||||
|
2. Before the container's nginx is started, the helper script
|
||||||
|
1. scans the `.env` file for known configuration variables and then
|
||||||
|
2. adds their values to `conf/runtime-config.js` which is sourced inside the application in the
|
||||||
|
immutable `window.runtime_config` object
|
||||||
|
|
||||||
|
During development, this `runtime-config.js` file is still loaded, but empty and thus the `getConfiguration()` ignores
|
||||||
|
it and prefers values from the sourced `.env.development`.
|
||||||
|
|
||||||
|
This means that new environment variables need to be added to all `.env*` files!
|
||||||
|
|
||||||
|
### Release
|
||||||
|
|
||||||
|
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
||||||
|
platform, uploads produced artifacts to that release and publishes docker images automatically.
|
||||||
|
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
||||||
|
built successfully:
|
||||||
|
|
||||||
|
* Adapt `package.json` and change `version` to the correct version number
|
||||||
|
* Invoke `npm install` once which properly sets the version inside the lock file
|
||||||
|
* Adapt language files, e.g., `en.json` and change `version` to the correct version number
|
||||||
|
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||||
|
in footer
|
||||||
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml`
|
||||||
|
|
||||||
|
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||||
|
|
||||||
|
* Adapt `package.json` and change `version` to the _next_ version number (semantic versioning applied, so `patch`
|
||||||
|
should bump patch level version and prepare branch for `develop` should bump minor or major version)
|
||||||
|
* Invoke `npm install` for each of those branches which properly sets the version inside the lock file
|
||||||
|
* Adapt language files, e.g., `en.json` and change `version` to the _next_ version number (semantic versioning
|
||||||
|
applied, so `patch` should bump patch level version and prepare branch for `develop` should bump minor or major
|
||||||
|
version)
|
||||||
|
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||||
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
0
server/web/ci/.gitkeep
Normal file
132
server/web/eslint.config.js
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import jsRecommendedLib from '@eslint/js';
|
||||||
|
import typescriptPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
|
import typescriptParser from '@typescript-eslint/parser';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||||
|
import prettierPlugin from 'eslint-plugin-prettier';
|
||||||
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
|
import sonarjsPlugin from 'eslint-plugin-sonarjs';
|
||||||
|
import testingLibPlugin from 'eslint-plugin-testing-library';
|
||||||
|
import { fixupPluginRules } from '@eslint/compat';
|
||||||
|
|
||||||
|
// eslint.config.js
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ['**/styles/**', '**/__tests__/**', '**/*.test.tsx', '**/*.test.ts', '*.less', 'src/**/*.tsx'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parser: typescriptParser
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
react: reactPlugin,
|
||||||
|
'react-hooks': fixupPluginRules(reactHooksPlugin),
|
||||||
|
sonarjs: sonarjsPlugin,
|
||||||
|
import: fixupPluginRules(importPlugin),
|
||||||
|
'jsx-a11y': jsxA11yPlugin,
|
||||||
|
'@typescript-eslint': typescriptPlugin,
|
||||||
|
prettier: prettierPlugin,
|
||||||
|
js: jsRecommendedLib,
|
||||||
|
'testing-library': fixupPluginRules(testingLibPlugin)
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect'
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {},
|
||||||
|
node: {
|
||||||
|
paths: ['src'],
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...jsRecommendedLib.configs.recommended.rules,
|
||||||
|
...reactPlugin.configs.recommended.rules,
|
||||||
|
...reactPlugin.configs['jsx-runtime'].rules,
|
||||||
|
...reactHooksPlugin.configs.recommended.rules,
|
||||||
|
...importPlugin.configs.recommended.rules,
|
||||||
|
...jsxA11yPlugin.flatConfigs.recommended.rules,
|
||||||
|
semi: 'error',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
'no-undef': 'off',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'testing-library/no-debugging-utils': 'warn',
|
||||||
|
'testing-library/no-dom-import': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/jsx-uses-react': 'off',
|
||||||
|
'no-console': 'warn',
|
||||||
|
'no-duplicate-imports': 'warn',
|
||||||
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
|
'sonarjs/cognitive-complexity': 'off',
|
||||||
|
'sonarjs/elseif-without-else': 'off',
|
||||||
|
'sonarjs/max-switch-cases': 'error',
|
||||||
|
'sonarjs/no-all-duplicated-branches': 'error',
|
||||||
|
'sonarjs/no-collapsible-if': 'error',
|
||||||
|
'sonarjs/no-collection-size-mischeck': 'error',
|
||||||
|
'sonarjs/no-duplicate-string': 'off',
|
||||||
|
'sonarjs/no-duplicated-branches': 'error',
|
||||||
|
'sonarjs/no-element-overwrite': 'error',
|
||||||
|
'sonarjs/no-empty-collection': 'error',
|
||||||
|
'sonarjs/no-extra-arguments': 'error',
|
||||||
|
'sonarjs/no-gratuitous-expressions': 'error',
|
||||||
|
'sonarjs/no-identical-conditions': 'error',
|
||||||
|
'sonarjs/no-identical-expressions': 'error',
|
||||||
|
'sonarjs/no-identical-functions': 'error',
|
||||||
|
'sonarjs/no-ignored-return': 'error',
|
||||||
|
'sonarjs/no-inverted-boolean-check': 'error',
|
||||||
|
'sonarjs/no-nested-switch': 'error',
|
||||||
|
'sonarjs/no-nested-template-literals': 'error',
|
||||||
|
'sonarjs/no-one-iteration-loop': 'error',
|
||||||
|
'sonarjs/no-redundant-boolean': 'error',
|
||||||
|
'sonarjs/no-redundant-jump': 'error',
|
||||||
|
'sonarjs/no-same-line-conditional': 'error',
|
||||||
|
'sonarjs/no-small-switch': 'error',
|
||||||
|
'sonarjs/no-unused-collection': 'error',
|
||||||
|
'sonarjs/no-use-of-empty-return-value': 'error',
|
||||||
|
'sonarjs/no-useless-catch': 'error',
|
||||||
|
'sonarjs/non-existent-operator': 'error',
|
||||||
|
'sonarjs/prefer-immediate-return': 'error',
|
||||||
|
'sonarjs/prefer-object-literal': 'error',
|
||||||
|
'sonarjs/prefer-single-boolean-return': 'error',
|
||||||
|
'sonarjs/prefer-while': 'error',
|
||||||
|
'import/order': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
alphabetize: {
|
||||||
|
caseInsensitive: true,
|
||||||
|
order: 'asc'
|
||||||
|
},
|
||||||
|
groups: [['builtin', 'external', 'index', 'sibling', 'parent', 'internal']],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
pathGroups: [
|
||||||
|
{
|
||||||
|
pattern: '*.less',
|
||||||
|
group: 'index',
|
||||||
|
patternOptions: {
|
||||||
|
matchBase: true
|
||||||
|
},
|
||||||
|
position: 'before'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: '*.json',
|
||||||
|
group: 'index',
|
||||||
|
patternOptions: {
|
||||||
|
matchBase: true
|
||||||
|
},
|
||||||
|
position: 'after'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
18
server/web/index.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/Favicon16x16.png" sizes="16x16" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/Favicon32x32.png" sizes="32x32" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<script type="module" src="/conf/runtime-config.js"></script>
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<title>upda</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28419
server/web/package-lock.json
generated
Normal file
116
server/web/package.json
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{
|
||||||
|
"name": "upda-ui",
|
||||||
|
"version": "4.0.1",
|
||||||
|
"private": true,
|
||||||
|
"author": "varakh@varakh.de",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20",
|
||||||
|
"npm": "^10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"serve": "vite preview",
|
||||||
|
"test": "vitest watch",
|
||||||
|
"test:no-watch": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"test:ci": "vitest run --coverage --passWithNoTests",
|
||||||
|
"format": "prettier --write ./src/**/*.{ts,tsx,less}",
|
||||||
|
"format:check": "prettier --check ./src/**/*.{ts,tsx,less}",
|
||||||
|
"lint": "eslint \"./src/**/*.{tsx,ts}\"",
|
||||||
|
"lintfix": "eslint --fix \"./src/**/*.{tsx,ts}\"",
|
||||||
|
"lint:style": "stylelint \"./src/**/*.less\"",
|
||||||
|
"lint-style-fix": "stylelint \"./src/**/*.less\" --fix",
|
||||||
|
"checkstyle": "npm run checkstyle:ts && npm run checkstyle:less && npm run checkstyle:format",
|
||||||
|
"checkstyle:format": "npm run format:check",
|
||||||
|
"checkstyle:ts": "eslint \"./src/**/*.{ts,tsx}\" -f checkstyle > ci/eslint.xml",
|
||||||
|
"checkstyle:less": "stylelint \"./src/**/*.less\"",
|
||||||
|
"clean": "npx --quiet rimraf build && npx --quiet rimraf node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
|
"@ant-design/pro-layout": "^7.21.1",
|
||||||
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
|
"@testing-library/react": "^16.0.1",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@uiw/react-md-editor": "^4.0.4",
|
||||||
|
"antd": "^5.21.5",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"html-react-parser": "^5.1.18",
|
||||||
|
"i18next": "^23.16.3",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"linkify-html": "^4.1.3",
|
||||||
|
"linkifyjs": "^4.1.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"moment-timezone": "^0.5.46",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-i18next": "^15.1.0",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
|
"react-router-dom": "^6.27.0",
|
||||||
|
"react-virtualized": "^9.22.5",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/register": "^7.25.9",
|
||||||
|
"@eslint/compat": "^1.2.1",
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/lodash": "^4.17.12",
|
||||||
|
"@types/node": "^20.16.5",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||||
|
"@types/react-virtualized": "^9.21.30",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
||||||
|
"@typescript-eslint/parser": "^8.11.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"@vitest/coverage-v8": "^2.1.3",
|
||||||
|
"babel-plugin-import": "^1.13.8",
|
||||||
|
"c8": "^10.1.2",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-formatter-checkstyle": "^8.40.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.3",
|
||||||
|
"eslint-plugin-disable": "^2.0.3",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-react": "^7.37.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-sonarjs": "^2.0.4",
|
||||||
|
"eslint-plugin-testing-library": "^6.4.0",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"less-loader": "^12.2.0",
|
||||||
|
"postcss-less": "^6.0.0",
|
||||||
|
"postcss-markdown": "^1.2.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"stylelint": "^16.10.0",
|
||||||
|
"stylelint-config-standard": "^36.0.1",
|
||||||
|
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
|
||||||
|
"stylelint-order": "^6.0.4",
|
||||||
|
"stylelint-prettier": "^5.0.2",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-eslint2": "^5.0.1",
|
||||||
|
"vite-plugin-stylelint": "^5.3.1",
|
||||||
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
|
"vite-tsconfig-paths": "^5.0.1",
|
||||||
|
"vitest": "^2.1.3"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
server/web/public/Favicon16x16.png
Normal file
After Width: | Height: | Size: 816 B |
BIN
server/web/public/Favicon32x32.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
7
server/web/public/conf/runtime-config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Note that Object.freeze is NOT recursive
|
||||||
|
const runtime_config = Object.freeze({});
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'runtime_config', {
|
||||||
|
value: runtime_config,
|
||||||
|
writable: false
|
||||||
|
});
|
BIN
server/web/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
8
server/web/public/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"short_name": "",
|
||||||
|
"name": "",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
server/web/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
15
server/web/src/App.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import getConfiguration from './getConfiguration';
|
||||||
|
import AppRouter from './router/AppRouter';
|
||||||
|
import { isDevelopment } from './utils/envHelper';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDevelopment()) {
|
||||||
|
document.title = getConfiguration().VITE_APP_TITLE;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <AppRouter />;
|
||||||
|
};
|
||||||
|
export default App;
|
8
server/web/src/__mocks__/config.mock.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const mockConfig = {};
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'runtime_config', {
|
||||||
|
writable: true,
|
||||||
|
value: mockConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
export { mockConfig };
|
4
server/web/src/__mocks__/react-i18next.mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: (): [(key: string) => string] => [(key: string): string => key]
|
||||||
|
}));
|
||||||
|
export {};
|
67
server/web/src/api/actionInvocationsApi.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { injectEndpoints } from './index';
|
||||||
|
import ActionInvocationFilterQueryParamNames from '../constants/api/actionInvocationFilterQueryParamNames';
|
||||||
|
import ApiTags from '../constants/apiTags';
|
||||||
|
import { ActionInvocationsRequestParams, ActionInvocationSingleResponse, ActionInvocationsResponse } from '../types';
|
||||||
|
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
|
const TAG_LIST_ID = 'LIST';
|
||||||
|
|
||||||
|
const invalidatesTags = (
|
||||||
|
results?: ActionInvocationsResponse | ActionInvocationSingleResponse | void,
|
||||||
|
error?: FetchBaseQueryError
|
||||||
|
) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [ApiTags.ActionInvocations] as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionInvocationsApi = injectEndpoints({
|
||||||
|
endpoints: (build) => {
|
||||||
|
return {
|
||||||
|
getActionInvocations: build.query<ActionInvocationsResponse, ActionInvocationsRequestParams>({
|
||||||
|
query: ({ page, pageSize, order, orderBy }) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (page) {
|
||||||
|
params.append(ActionInvocationFilterQueryParamNames.PAGE, `${page}`);
|
||||||
|
}
|
||||||
|
if (pageSize) {
|
||||||
|
params.append(ActionInvocationFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||||
|
}
|
||||||
|
if (order) {
|
||||||
|
params.append(ActionInvocationFilterQueryParamNames.ORDER, `${order}`);
|
||||||
|
}
|
||||||
|
if (orderBy) {
|
||||||
|
params.append(ActionInvocationFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||||
|
}
|
||||||
|
return { url: `action-invocations?${params.toString()}` };
|
||||||
|
},
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data.content) {
|
||||||
|
return [
|
||||||
|
{ type: ApiTags.ActionInvocations, id: TAG_LIST_ID },
|
||||||
|
...result.data.content.map(({ id }) => ({ type: ApiTags.ActionInvocations, id }))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getActionInvocationById: build.query<ActionInvocationSingleResponse, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `action-invocations/${id}` }),
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data) {
|
||||||
|
return [{ type: ApiTags.ActionInvocations, id: result.data.id }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteActionInvocation: build.mutation<void, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `action-invocations/${id}`, method: 'DELETE' }),
|
||||||
|
invalidatesTags
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetActionInvocationsQuery, useGetActionInvocationByIdQuery, useDeleteActionInvocationMutation } =
|
||||||
|
actionInvocationsApi;
|
197
server/web/src/api/actionsApi.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import { injectEndpoints } from './index';
|
||||||
|
import ActionFilterQueryParamNames from '../constants/api/actionFilterQueryParamNames';
|
||||||
|
import ApiTags from '../constants/apiTags';
|
||||||
|
import {
|
||||||
|
ActionSingleResponse,
|
||||||
|
ActionsRequestParams,
|
||||||
|
ActionsResponse,
|
||||||
|
ActionTestSingleResponse,
|
||||||
|
CreateActionRequest,
|
||||||
|
ModifyActionEnabledRequest,
|
||||||
|
ModifyActionLabelRequest,
|
||||||
|
ModifyActionMatchApplicationRequest,
|
||||||
|
ModifyActionMatchEventRequest,
|
||||||
|
ModifyActionMatchHostRequest,
|
||||||
|
ModifyActionMatchProviderRequest,
|
||||||
|
ModifyActionPayloadRequest,
|
||||||
|
TestActionRequest
|
||||||
|
} from '../types';
|
||||||
|
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
|
const TAG_LIST_ID = 'LIST';
|
||||||
|
|
||||||
|
const invalidatesTags = (results?: ActionsResponse | ActionSingleResponse | void, error?: FetchBaseQueryError) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [ApiTags.Actions] as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionsApi = injectEndpoints({
|
||||||
|
endpoints: (build) => {
|
||||||
|
return {
|
||||||
|
getActions: build.query<ActionsResponse, ActionsRequestParams>({
|
||||||
|
query: ({ page, pageSize, order, orderBy }) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (page) {
|
||||||
|
params.append(ActionFilterQueryParamNames.PAGE, `${page}`);
|
||||||
|
}
|
||||||
|
if (pageSize) {
|
||||||
|
params.append(ActionFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||||
|
}
|
||||||
|
if (order) {
|
||||||
|
params.append(ActionFilterQueryParamNames.ORDER, `${order}`);
|
||||||
|
}
|
||||||
|
if (orderBy) {
|
||||||
|
params.append(ActionFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||||
|
}
|
||||||
|
return { url: `actions?${params.toString()}` };
|
||||||
|
},
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data.content) {
|
||||||
|
return [
|
||||||
|
{ type: ApiTags.Actions, id: TAG_LIST_ID },
|
||||||
|
...result.data.content.map(({ id }) => ({ type: ApiTags.Actions, id }))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getActionById: build.query<ActionSingleResponse, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `actions/${id}` }),
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data) {
|
||||||
|
return [{ type: ApiTags.Actions, id: result.data.id }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createAction: build.mutation<ActionSingleResponse, CreateActionRequest>({
|
||||||
|
query: (body) => ({ url: 'actions', method: 'POST', body }),
|
||||||
|
invalidatesTags
|
||||||
|
}),
|
||||||
|
testAction: build.mutation<ActionTestSingleResponse, { id: string; body: TestActionRequest }>({
|
||||||
|
query: ({ id, body }) => ({ url: `actions/${id}/test`, method: 'POST', body })
|
||||||
|
}),
|
||||||
|
modifyLabelAction: build.mutation<ActionSingleResponse, { id: string; body: ModifyActionLabelRequest }>({
|
||||||
|
query: ({ id, body }) => ({ url: `actions/${id}/label`, method: 'PATCH', body }),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyMatchApplicationAction: build.mutation<
|
||||||
|
ActionSingleResponse,
|
||||||
|
{ id: string; body: ModifyActionMatchApplicationRequest }
|
||||||
|
>({
|
||||||
|
query: ({ id, body }) => ({
|
||||||
|
url: `actions/${id}/match-application`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyMatchHostAction: build.mutation<
|
||||||
|
ActionSingleResponse,
|
||||||
|
{ id: string; body: ModifyActionMatchHostRequest }
|
||||||
|
>({
|
||||||
|
query: ({ id, body }) => ({
|
||||||
|
url: `actions/${id}/match-host`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyMatchEventAction: build.mutation<
|
||||||
|
ActionSingleResponse,
|
||||||
|
{ id: string; body: ModifyActionMatchEventRequest }
|
||||||
|
>({
|
||||||
|
query: ({ id, body }) => ({
|
||||||
|
url: `actions/${id}/match-event`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyMatchProviderAction: build.mutation<
|
||||||
|
ActionSingleResponse,
|
||||||
|
{ id: string; body: ModifyActionMatchProviderRequest }
|
||||||
|
>({
|
||||||
|
query: ({ id, body }) => ({
|
||||||
|
url: `actions/${id}/match-provider`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyTypeAndPayloadAction: build.mutation<
|
||||||
|
ActionSingleResponse,
|
||||||
|
{ id: string; body: ModifyActionPayloadRequest }
|
||||||
|
>({
|
||||||
|
query: ({ id, body }) => ({
|
||||||
|
url: `actions/${id}/payload`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
modifyEnabledAction: build.mutation<ActionSingleResponse, { id: string; body: ModifyActionEnabledRequest }>(
|
||||||
|
{
|
||||||
|
query: ({ id, body }) => ({ url: `actions/${id}/enabled`, method: 'PATCH', body }),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
deleteAction: build.mutation<void, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `actions/${id}`, method: 'DELETE' }),
|
||||||
|
invalidatesTags
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetActionsQuery,
|
||||||
|
useGetActionByIdQuery,
|
||||||
|
useDeleteActionMutation,
|
||||||
|
useModifyLabelActionMutation,
|
||||||
|
useModifyMatchEventActionMutation,
|
||||||
|
useModifyMatchApplicationActionMutation,
|
||||||
|
useModifyMatchHostActionMutation,
|
||||||
|
useModifyMatchProviderActionMutation,
|
||||||
|
useModifyTypeAndPayloadActionMutation,
|
||||||
|
useModifyEnabledActionMutation,
|
||||||
|
useCreateActionMutation,
|
||||||
|
useTestActionMutation
|
||||||
|
} = actionsApi;
|
60
server/web/src/api/eventsApi.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { injectEndpoints } from './index';
|
||||||
|
import EventFilterQueryParamNames from '../constants/api/eventFilterQueryParamNames';
|
||||||
|
import ApiTags from '../constants/apiTags';
|
||||||
|
import { EventSingleResponse, EventsRequestParams, EventsResponse } from '../types/event';
|
||||||
|
|
||||||
|
const TAG_LIST_ID = 'LIST';
|
||||||
|
|
||||||
|
export const eventsApi = injectEndpoints({
|
||||||
|
endpoints: (build) => {
|
||||||
|
return {
|
||||||
|
getEvents: build.query<EventsResponse, EventsRequestParams>({
|
||||||
|
query: ({ size, skip, order, orderBy }) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (size) {
|
||||||
|
params.append(EventFilterQueryParamNames.SIZE, `${size}`);
|
||||||
|
}
|
||||||
|
if (skip) {
|
||||||
|
params.append(EventFilterQueryParamNames.SKIP, `${skip}`);
|
||||||
|
}
|
||||||
|
if (order) {
|
||||||
|
params.append(EventFilterQueryParamNames.ORDER, `${order}`);
|
||||||
|
}
|
||||||
|
if (orderBy) {
|
||||||
|
params.append(EventFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||||
|
}
|
||||||
|
return { url: `events?${params.toString()}` };
|
||||||
|
},
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data.content) {
|
||||||
|
return [
|
||||||
|
{ type: ApiTags.Events, id: TAG_LIST_ID },
|
||||||
|
...result.data.content.map(({ id }) => ({ type: ApiTags.Events, id }))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getEventById: build.query<EventSingleResponse, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `events/${id}` }),
|
||||||
|
providesTags: (result, error) => {
|
||||||
|
if (!error && result?.data) {
|
||||||
|
return [{ type: ApiTags.Events, id: result.data.id }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteEvent: build.mutation<void, { id: string }>({
|
||||||
|
query: ({ id }) => ({ url: `events/${id}`, method: 'DELETE' }),
|
||||||
|
invalidatesTags: (result, error, arg) => {
|
||||||
|
if (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{ type: ApiTags.Events, id: arg.id }];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useLazyGetEventsQuery, useGetEventByIdQuery, useDeleteEventMutation } = eventsApi;
|