feat(actions): Add actions and secrets (with proper asynchronous enqueue and dequeue mechanism) (#22)
All checks were successful
/ build (push) Successful in 3m37s

Reviewed-on: #22
Co-authored-by: Varakh <varakh@varakh.de>
Co-committed-by: Varakh <varakh@varakh.de>
This commit is contained in:
Varakh 2024-04-26 12:37:50 +00:00 committed by Varakh
parent 1d4910d62b
commit 3532af74f2
44 changed files with 6665 additions and 1014 deletions

View file

@ -3,8 +3,8 @@ on:
tags:
- '*'
env:
VERSION_MAJOR: 1
VERSION_MINOR: 1
VERSION_MAJOR: 2
VERSION_MINOR: 0
VERSION_PATCH: 0
IMAGE_TAG: varakh/upda
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda

View file

@ -2,10 +2,16 @@
Changes adhere to [semantic versioning](https://semver.org).
## [1.1.0] - UNRELEASED
## [2.0.0] - UNRELEASED
> This is a major version upgrade. Other versions are incompatible with this release.
* Added mandatory `SECRET` environment variable to encrypt some data inside the database
* Switched to encrypting webhook tokens in database
* Added _Actions_, a simple way to trigger notifications via [shoutrrr](https://containrrr.dev/shoutrrr) which supports secrets
* Switched to producing events only for _Updates_
* Adapted logging which defaults to JSON encoding
* ...
* Updated dependencies
## [1.0.3] - 2024/01/21

228
README.md
View file

@ -23,6 +23,7 @@ Contributions are very welcome!
* [Configuration](#configuration)
* [3rd party integrations](#3rd-party-integrations)
* [Webhooks](#webhooks)
* [Actions](#actions)
* [Prometheus Metrics](#prometheus-metrics)
* [Deployment](#deployment)
* [Native](#native)
@ -59,21 +60,23 @@ 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.
> _upda_ is **NOT** a scraper to watch docker registries or GitHub releases, it simply tracks and consolidates updates
> from different sources provided via _webhooks_. If you like to watch GitHub releases, write a scraper and
> use `upda-cli` to report back to _upda_.
## 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).
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](#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:
@ -127,52 +130,68 @@ via web interface or API.
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`. 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 `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 |
| 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_ |
| `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`. 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 |
## 3rd party integrations
@ -182,8 +201,10 @@ This is the core mechanism of _upda_ and why it exists. Webhooks are the central
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
the [duin webhook notification configuration](https://crazymax.dev/diun/notif/webhook/), create a new _upda_ webhook via
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>`
@ -204,6 +225,64 @@ notif:
timeout: 10s
```
### 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 condition meet the action's definition (event name, host, application,
provider).
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.
Supported events 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_pending` | An update's state changed to pending |
| `update_updated_state_approved` | An update's state changed to approved |
| `update_updated_state_ignored` | An update's state changed to ignored |
| `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.
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 |
#### 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>`.
A full payload for defining an upda shoutrrr action looks like the following. No worries, there's
a [web interface](https://git.myservermanager.com/varakh/upda-ui) for configuring actions:
```json5
{
// ...
"type": "shoutrrr",
"matchEvent": "update_created",
// payload 'urls' and 'body' are specific to the shoutrrr action type
"payload": {
"urls": [
"gotify://myurl/<SECRET>GOTIFY_TOKEN</SECRET>/?title=Great+News+On+Upda"
],
"body": "A new update arrived on <VAR>HOST</VAR> for <VAR>APPLICATION</VAR>. Its version is <VAR>VERSION</VAR>."
}
}
```
### Prometheus Metrics
When `PROMETHEUS_ENABLED` is set to `true`, default metrics about memory utilization, but also custom metrics specific
@ -224,11 +303,6 @@ 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
@ -241,6 +315,8 @@ upda_updates_pending 1
upda_webhooks 2
# HELP upda_events amount of all events
upda_events 146
# HELP upda_actions amount of all actions
upda_actions 0
```
There's an example [Grafana](https://grafana.com) dashboard in the `_doc/` folder.
@ -248,17 +324,17 @@ There's an example [Grafana](https://grafana.com) dashboard in the `_doc/` folde
[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 }}"
- 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

View file

@ -60,6 +60,8 @@ services:
- DB_POSTGRES_PASSWORD=upda
- ADMIN_USER=admin
- ADMIN_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
@ -121,6 +123,8 @@ services:
- TZ=Europe/Berlin
- ADMIN_USER=admin
- ADMIN_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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -39,16 +39,12 @@ func (e WebhookType) Value() string {
type EventName string
const (
EventNameUpdateCreated EventName = "update_created"
EventNameUpdateUpdated EventName = "update_updated"
EventNameUpdateUpdatedPending EventName = "update_updated_state_pending"
EventNameUpdateUpdatedApproved EventName = "update_updated_state_approved"
EventNameUpdateUpdatedIgnored EventName = "update_updated_state_ignored"
EventNameUpdateDeleted EventName = "update_deleted"
EventNameWebhookCreated EventName = "webhook_created"
EventNameWebhookUpdatedLabel EventName = "webhook_updated_label"
EventNameWebhookUpdatedIgnoreHost EventName = "webhook_updated_ignore_host"
EventNameWebhookDeleted EventName = "webhook_deleted"
EventNameUpdateCreated EventName = "update_created"
EventNameUpdateUpdated EventName = "update_updated"
EventNameUpdateUpdatedPending EventName = "update_updated_state_pending"
EventNameUpdateUpdatedApproved EventName = "update_updated_state_approved"
EventNameUpdateUpdatedIgnored EventName = "update_updated_state_ignored"
EventNameUpdateDeleted EventName = "update_deleted"
)
func (e *EventName) Scan(value interface{}) error {
@ -64,7 +60,8 @@ func (e EventName) Value() string {
type EventState string
const (
EventStateCreated EventState = "created"
EventStateCreated EventState = "created"
EventStateEnqueued EventState = "enqueued"
)
func (e *EventState) Scan(value interface{}) error {
@ -75,3 +72,39 @@ func (e *EventState) Scan(value interface{}) error {
func (e EventState) Value() string {
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)
}

View file

@ -25,6 +25,62 @@ type CreateWebhookRequest struct {
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"`
}
type PaginateUpdateRequest struct {
PageSize int `form:"pageSize,default=5" binding:"numeric,gte=1"`
Page int `form:"page,default=1" binding:"numeric,gte=1"`
@ -35,10 +91,24 @@ type PaginateUpdateRequest 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"`
Page int `form:"page,default=1" binding:"numeric,gte=1"`
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 {
@ -216,12 +286,25 @@ func NewWebhookPageResponse(content []*WebhookResponse, page int, pageSize int,
type EventResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
State string `json:"state"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
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 {
Content []*EventResponse `json:"content"`
Size int `json:"size"`
@ -269,24 +352,160 @@ func NewEventWindowResponse(content []*EventResponse, size int, skip int, orderB
return e
}
type EventPayloadWebhookCreatedDto struct {
ID uuid.UUID `json:"id,omitempty"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
IgnoreHost bool `json:"ignoreHost"`
type SecretResponse struct {
ID uuid.UUID `json:"id"`
Key string `json:"key"`
Value string `json:"value,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type EventPayloadWebhookUpdatedDto struct {
ID uuid.UUID `json:"id,omitempty"`
LabelPrior string `json:"labelPrior,omitempty"`
Label string `json:"label,omitempty"`
IgnoreHostPrior bool `json:"ignoreHostPrior"`
IgnoreHost bool `json:"ignoreHost"`
Type string `json:"type,omitempty"`
type SecretSingleResponse struct {
Data SecretResponse `json:"data"`
}
type EventPayloadWebhookDeletedDto struct {
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
IgnoreHost bool `json:"ignoreHost"`
func NewSecretSingleResponse(id uuid.UUID, key string, value string, createdAt time.Time, updatedAt time.Time) *SecretSingleResponse {
e := new(SecretSingleResponse)
e.Data.ID = id
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
go.mod
View file

@ -5,6 +5,7 @@ go 1.21
require (
github.com/Depado/ginprom v1.8.1
github.com/adrg/xdg v0.4.0
github.com/containrrr/shoutrrr v0.8.0
github.com/gin-contrib/cors v1.7.1
github.com/gin-contrib/zap v1.1.1
github.com/gin-gonic/gin v1.9.1
@ -32,13 +33,13 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.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.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -49,9 +50,9 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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/reflect2 v1.0.2 // indirect

127
go.sum
View file

@ -2,8 +2,6 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
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/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Depado/ginprom v1.8.0 h1:zaaibRLNI1dMiiuj1MKzatm8qrcHzikMlCc1anqOdyo=
github.com/Depado/ginprom v1.8.0/go.mod h1:XBaKzeNBqPF4vxJpNLincSQZeMDnZp1tIbU0FU0UKgg=
github.com/Depado/ginprom v1.8.1 h1:lrQTddbRqlHq1j6SpJDySDumJlR7FEybzdX0PS3HXPc=
github.com/Depado/ginprom v1.8.1/go.mod h1:9Z+ahPJLSeMndDfnDTfiuBn2SKVAuL2yvihApWzof9A=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
@ -23,10 +21,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
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.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
@ -37,7 +31,6 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
@ -45,6 +38,8 @@ github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAA
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/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/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@ -63,22 +58,14 @@ 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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
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/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
github.com/gin-contrib/cors v1.6.0 h1:0Z7D/bVhE6ja07lI8CTjTonp6SB07o8bNuFyRbsBUQg=
github.com/gin-contrib/cors v1.6.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs=
github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps=
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/zap v0.2.0 h1:HLvt3rZXyC8XC+s2lHzMFow3UDqiEbfrBWJyHHS6L8A=
github.com/gin-contrib/zap v0.2.0/go.mod h1:eqfbe9ZmI+GgTZF6nRiC2ZwDeM4DK1Viwc8OxTCphh0=
github.com/gin-contrib/zap v1.1.0 h1:GWzL9+zmK8OJdiycaK2SK1/D3SZIYpieJDD0QCNAU1o=
github.com/gin-contrib/zap v1.1.0/go.mod h1:KzROP9rAL7ofFd1P8lx7Oo2lerwPWNL5vv4f6U/mAk8=
github.com/gin-contrib/zap v1.1.1 h1:DDyIF9YQorl3gZzAabIowRywHJuohDfiLnhwvWKl6SY=
github.com/gin-contrib/zap v1.1.1/go.mod h1:YW8KOko2kYLy8g6k9YgVNTj7SIcrUEzYiAd9IjiBPs0=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@ -87,6 +74,8 @@ github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-co-op/gocron-redis-lock v1.3.0 h1:PKwtuc/BhrDll/DxJfnXoW/+D1VXubd47xcGaB9pDuM=
github.com/go-co-op/gocron-redis-lock v1.3.0/go.mod h1:9+H7ZfqVtJfx94uEAELwH+uHkn1UpM6lRM99wOBTGtg=
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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -95,12 +84,6 @@ 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/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/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
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-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
@ -111,29 +94,24 @@ github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F4
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-redsync/redsync/v4 v4.11.0 h1:OPEcAxHBb95EzfwCKWM93ksOwHd5bTce2BD4+R14N6k=
github.com/go-redsync/redsync/v4 v4.11.0/go.mod h1:ZfayzutkgeBmEmBlUR3j+rF6kN44UUGtEdfzhBFZTPc=
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA=
github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
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.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
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=
@ -147,6 +125,8 @@ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/
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.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@ -157,8 +137,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
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/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.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
@ -171,22 +149,19 @@ 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
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/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
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-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
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=
@ -202,16 +177,16 @@ 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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
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/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/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
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/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -222,28 +197,14 @@ 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/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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.5.0 h1:Xe9TKMmZv939gwTBcvc0n1tzK5l2re0pKw/W/tN3amw=
github.com/redis/go-redis/v9 v9.5.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
@ -272,7 +233,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@ -288,12 +248,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
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/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.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
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.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@ -307,31 +263,22 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
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.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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.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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
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.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
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.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
@ -350,13 +297,9 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -372,12 +315,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
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.0.0-20220811171246-fbc7d0a398ab/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@ -385,7 +326,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
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/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -393,11 +333,9 @@ 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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=
@ -409,18 +347,11 @@ 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-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=
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/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -433,23 +364,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU=
gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
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.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=

View 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)
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(headerContentType, headerContentTypeApplicationJson)
c.Status(http.StatusNoContent)
}

View 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})
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)
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(headerContentType, headerContentTypeApplicationJson)
c.Status(http.StatusNoContent)
}

View file

@ -35,7 +35,6 @@ func (h *eventHandler) window(c *gin.Context) {
data = append(data, &api.EventResponse{
ID: e.ID,
Name: e.Name,
State: e.State,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Payload: e.Payload,
@ -51,6 +50,16 @@ 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)))
}
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) {
if err := h.service.delete(c.Param("id")); err != nil {
_ = c.AbortWithError(errToHttpStatus(err), err)

View 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)
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(headerContentType, headerContentTypeApplicationJson)
c.Status(http.StatusNoContent)
}

View file

@ -82,7 +82,7 @@ func (h *updateHandler) updateState(c *gin.Context) {
var req api.ModifyUpdateStateRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err = c.ShouldBindJSON(&req); err != nil {
errAbortWithValidatorPayload(c, err)
return
}

View file

@ -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)))
}
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) {
var e *Webhook
var err error
var req api.CreateWebhookRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err = c.ShouldBindJSON(&req); err != nil {
errAbortWithValidatorPayload(c, err)
return
}
@ -77,7 +87,7 @@ func (h *webhookHandler) updateLabel(c *gin.Context) {
var req api.ModifyWebhookLabelRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err = c.ShouldBindJSON(&req); err != nil {
errAbortWithValidatorPayload(c, err)
return
}
@ -96,7 +106,7 @@ func (h *webhookHandler) updateIgnoreHost(c *gin.Context) {
var req api.ModifyWebhookIgnoreHostRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err = c.ShouldBindJSON(&req); err != nil {
errAbortWithValidatorPayload(c, err)
return
}

View file

@ -45,15 +45,22 @@ func Start() {
updateRepo := newUpdateDbRepo(env.db)
webhookRepo := newWebhookDbRepo(env.db)
eventRepo := newEventDbRepo(env.db)
secretRepo := newSecretDbRepo(env.db)
actionRepo := newActionDbRepo(env.db)
actionInvocationRepo := newActionInvocationDbRepo(env.db)
lockService := newLockMemService()
eventService := newEventService(eventRepo)
updateService := newUpdateService(updateRepo, eventService, prometheusService)
webhookService := newWebhookService(webhookRepo, env.webhookConfig, eventService)
updateService := newUpdateService(updateRepo, eventService)
webhookService := newWebhookService(webhookRepo, env.webhookConfig)
webhookInvocationService := newWebhookInvocationService(webhookService, updateService, env.webhookConfig)
taskService := newTaskService(updateService, eventService, webhookService, lockService, prometheusService, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig)
secretService := newSecretService(secretRepo)
actionService := newActionService(actionRepo, eventService)
actionInvocationService := newActionInvocationService(actionInvocationRepo, actionService, eventService, secretService)
taskService := newTaskService(updateService, eventService, webhookService, actionService, actionInvocationService, lockService, prometheusService, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig)
taskService.init()
taskService.start()
@ -61,6 +68,10 @@ func Start() {
webhookHandler := newWebhookHandler(webhookService)
webhookInvocationHandler := newWebhookInvocationHandler(webhookInvocationService, webhookService)
eventHandler := newEventHandler(eventService)
secretHandler := newSecretHandler(secretService)
actionHandler := newActionHandler(actionService)
actionInvocationHandler := newActionInvocationHandler(actionService, actionInvocationService)
infoHandler := newInfoHandler(env.appConfig)
healthHandler := newHealthHandler()
authHandler := newAuthHandler()
@ -97,13 +108,38 @@ func Start() {
apiAuthGroup.GET("/webhooks", webhookHandler.paginate)
apiAuthGroup.POST("/webhooks", webhookHandler.create)
apiAuthGroup.GET("/webhooks/:id", webhookHandler.get)
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("/events/:id", eventHandler.get)
apiAuthGroup.DELETE("/events/:id", eventHandler.delete)
apiAuthGroup.GET("/secrets", secretHandler.getAll)
apiAuthGroup.GET("/secrets/:id", secretHandler.get)
apiAuthGroup.POST("/secrets", secretHandler.create)
apiAuthGroup.PATCH("/secrets/:id/value", secretHandler.updateValue)
apiAuthGroup.DELETE("/secrets/:id", secretHandler.delete)
apiAuthGroup.GET("/actions", actionHandler.paginate)
apiAuthGroup.POST("/actions", actionHandler.create)
apiAuthGroup.GET("/actions/:id", actionHandler.get)
apiAuthGroup.PATCH("/actions/:id/label", actionHandler.updateLabel)
apiAuthGroup.PATCH("/actions/:id/match-event", actionHandler.updateMatchEvent)
apiAuthGroup.PATCH("/actions/:id/match-host", actionHandler.updateMatchHost)
apiAuthGroup.PATCH("/actions/:id/match-application", actionHandler.updateMatchApplication)
apiAuthGroup.PATCH("/actions/:id/match-provider", actionHandler.updateMatchProvider)
apiAuthGroup.PATCH("/actions/:id/payload", actionHandler.updatePayload)
apiAuthGroup.PATCH("/actions/:id/enabled", actionHandler.updateEnabled)
apiAuthGroup.DELETE("/actions/:id", actionHandler.delete)
apiAuthGroup.POST("/actions/:id/test", actionInvocationHandler.test)
apiAuthGroup.GET("/action-invocations", actionInvocationHandler.paginate)
apiAuthGroup.GET("/action-invocations/:id", actionInvocationHandler.get)
apiAuthGroup.DELETE("/action-invocations/:id", actionInvocationHandler.delete)
// start server
serverAddress := fmt.Sprintf("%s:%d", env.serverConfig.listen, env.serverConfig.port)
srv := &http.Server{

View file

@ -2,5 +2,5 @@ package server
const (
Name = "upda"
Version = "1.1.0"
Version = "2.0.0"
)

View file

@ -11,6 +11,8 @@ const (
envLoggingDirectory = "LOGGING_DIRECTORY"
loggingFileNameDefault = "upda.log"
envSecret = "SECRET"
envTZ = "TZ"
tzDefault = "Europe/Berlin"
@ -70,7 +72,7 @@ const (
envTaskUpdateCleanStaleMaxAge = "TASK_UPDATE_CLEAN_STALE_MAX_AGE"
taskUpdateCleanStaleEnabledDefault = "false"
taskUpdateCleanStaleIntervalDefault = "1h"
taskUpdateCleanStaleMaxAgeDefault = "168h"
taskUpdateCleanStaleMaxAgeDefault = "720h"
envTaskEventCleanStaleEnabled = "TASK_EVENT_CLEAN_STALE_ENABLED"
envTaskEventCleanStaleInterval = "TASK_EVENT_CLEAN_STALE_INTERVAL"
@ -79,6 +81,29 @@ const (
taskEventCleanStaleIntervalDefault = "8h"
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"
envLockRedisUrl = "LOCK_REDIS_URL"
redisEnabledDefault = "false"

View file

@ -13,12 +13,12 @@ const (
metricUpdatesApproved = "updates_approved"
metricUpdatesApprovedHelp = "amount of all updates in approved state"
metricUpdates = "updates"
metricUpdatesHelp = "details for all updates, 0=pending, 1=approved, 2=ignored"
metricWebhooks = "webhooks"
metricWebhooksHelp = "amount of all webhooks"
metricEvents = "events"
metricEventsHelp = "amount of all events"
metricActions = "actions"
metricActionsHelp = "amount of all actions"
)

15
server/dto.go Normal file
View file

@ -0,0 +1,15 @@
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
}

View file

@ -1,8 +1,10 @@
package server
import (
"git.myservermanager.com/varakh/upda/util"
"github.com/google/uuid"
"gorm.io/gorm"
"os"
"time"
)
@ -11,16 +13,6 @@ func (u *Update) BeforeCreate(tx *gorm.DB) (err error) {
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
type Update struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
@ -34,6 +26,32 @@ type Update struct {
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
}
// BeforeCreate encrypts secret value 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
}
// AfterSave decrypt secret value 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
}
// Webhook entity holding information for webhooks
type Webhook struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
@ -45,6 +63,11 @@ type Webhook struct {
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
type Event struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
@ -54,3 +77,102 @@ type Event struct {
CreatedAt time.Time `gorm:"time;autoCreateTime;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"`
}

View file

@ -49,6 +49,16 @@ type taskConfig struct {
eventCleanStaleEnabled bool
eventCleanStaleInterval string
eventCleanStaleMaxAge time.Duration
actionsEnqueueEnabled bool
actionsEnqueueInterval string
actionsEnqueueBatchSize int
actionsInvokeEnabled bool
actionsInvokeInterval string
actionsInvokeBatchSize int
actionsInvokeMaxRetries int
actionsCleanStaleEnabled bool
actionsCleanStaleInterval string
actionsCleanStaleMaxAge time.Duration
prometheusRefreshInterval string
}
@ -217,6 +227,35 @@ func bootstrapEnvironment() *Environment {
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale events. Reason: %s", errParse.Error())
}
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 actionsInvokeBatchSize int
if actionsInvokeBatchSize, err = strconv.Atoi(os.Getenv(envTaskActionsInvokeBatchSize)); err != nil {
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.")
}
var actionsCleanStaleMaxAge time.Duration
if actionsCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskActionsCleanStaleMaxAge)); errParse != nil {
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale actions. Reason: %s", errParse.Error())
}
tc = &taskConfig{
updateCleanStaleEnabled: os.Getenv(envTaskUpdateCleanStaleEnabled) == "true",
updateCleanStaleInterval: os.Getenv(envTaskUpdateCleanStaleInterval),
@ -224,6 +263,16 @@ func bootstrapEnvironment() *Environment {
eventCleanStaleEnabled: os.Getenv(envTaskEventCleanStaleEnabled) == "true",
eventCleanStaleInterval: os.Getenv(envTaskEventCleanStaleInterval),
eventCleanStaleMaxAge: eventCleanStaleMaxAge,
actionsEnqueueEnabled: os.Getenv(envTaskActionsEnqueueEnabled) == "true",
actionsEnqueueInterval: os.Getenv(envTaskActionsEnqueueInterval),
actionsEnqueueBatchSize: actionsEnqueueBatchSize,
actionsInvokeEnabled: os.Getenv(envTaskActionsInvokeEnabled) == "true",
actionsInvokeInterval: os.Getenv(envTaskActionsInvokeInterval),
actionsInvokeBatchSize: actionsInvokeBatchSize,
actionsInvokeMaxRetries: actionsInvokeMaxRetries,
actionsCleanStaleEnabled: os.Getenv(envTaskActionsCleanStaleEnabled) == "true",
actionsCleanStaleInterval: os.Getenv(envTaskActionsCleanStaleInterval),
actionsCleanStaleMaxAge: actionsCleanStaleMaxAge,
prometheusRefreshInterval: os.Getenv(envTaskPrometheusRefreshInterval),
}
@ -289,7 +338,7 @@ func bootstrapEnvironment() *Environment {
}
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()
@ -328,7 +377,7 @@ func bootstrapEnvironment() *Environment {
prometheusConfig: pc,
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)
}
@ -344,6 +393,7 @@ func bootstrapFromEnvironmentAndValidate() {
// app
setEnvKeyDefault(envTZ, tzDefault)
failIfEnvKeyNotPresent(envSecret)
failIfEnvKeyNotPresent(envAdminUser)
failIfEnvKeyNotPresent(envAdminPassword)
@ -362,6 +412,19 @@ func bootstrapFromEnvironmentAndValidate() {
setEnvKeyDefault(envTaskEventCleanStaleInterval, taskEventCleanStaleIntervalDefault)
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)
// prometheus

View file

@ -6,10 +6,13 @@ import (
)
var (
errorValidationNotEmpty = newServiceError(IllegalArgument, errors.New("assert: empty 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"))
errorValidationPageSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: pageSize has to be greater 0"))
errorValidationNotEmpty = newServiceError(IllegalArgument, errors.New("assert: empty 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"))
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"))
errorResourceAccessDenied = newServiceError(Forbidden, errors.New("resource access denied"))

375
server/repository_action.go Normal file
View 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
}

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

View file

@ -12,7 +12,9 @@ type eventRepository interface {
window(size int, skip int, orderBy string, order string) ([]*Event, error)
windowHasNext(size int, skip int, orderBy string, order string) (bool, 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)
updateState(id string, state api.EventState) (*Event, error)
delete(id string) (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
}
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) {
if id == "" {
return 0, errorValidationNotBlank
@ -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) {
var e []*Event
if size <= 0 {
return nil, errorValidationSizeGreaterZero
}
if orderBy == "" {
orderBy = "created_at"
}
@ -121,6 +149,7 @@ func (r *eventDbRepo) window(size int, skip int, orderBy string, order string) (
order = "asc"
}
var e []*Event
if res := r.db.Order(orderBy + " " + order).Offset(skip).Limit(size).Find(&e); res.Error != nil {
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
}
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) {
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)
if len(state) > 0 {
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 0, newServiceDatabaseError(res.Error)
}
return c, nil
return states
}
func criterionEventState(states []string) func(db *gorm.DB) *gorm.DB {

131
server/repository_secret.go Normal file
View 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
}

View file

@ -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) {
if page == 0 || pageSize <= 0 {
if page == 0 {
return nil, errorValidationPageGreaterZero
}
if pageSize <= 0 {

View file

@ -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) {
if page == 0 || pageSize <= 0 {
if page == 0 {
return nil, errorValidationPageGreaterZero
}
if pageSize <= 0 {

264
server/service_action.go Normal file
View 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)
}

View file

@ -0,0 +1,402 @@
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
}
var filteredActions []*Action
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)
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...)
}

View file

@ -1,7 +1,9 @@
package server
import (
"errors"
"git.myservermanager.com/varakh/upda/api"
"git.myservermanager.com/varakh/upda/util"
"go.uber.org/zap"
"time"
)
@ -85,60 +87,6 @@ func (s *eventService) createUpdateDeleted(e *Update) *Event {
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
}
func (s *eventService) createWithWarnOnly(name api.EventName, payload interface{}) *Event {
var e *Event
var err error
@ -212,3 +160,88 @@ func (s *eventService) windowHasNext(size int, skip int, orderBy string, order s
func (s *eventService) count(state ...api.EventState) (int64, error) {
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}, 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}, nil
case api.EventNameUpdateUpdatedApproved.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}, nil
case api.EventNameUpdateUpdatedPending.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}, nil
case api.EventNameUpdateUpdatedIgnored.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}, 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}, nil
}
return nil, newServiceError(General, errors.New("no matching event found"))
}

View file

@ -53,10 +53,10 @@ func (s *prometheusService) init() {
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)
err = s.registerGaugeNoLabels(metricActions, metricActionsHelp)
if err != nil {
zap.L().Sugar().Fatalf("Cannot initialize service. Reason: %v", err)

108
server/service_secret.go Normal file
View 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
}

View file

@ -10,25 +10,34 @@ import (
)
type taskService struct {
updateService *updateService
eventService *eventService
webhookService *webhookService
lockService lockService
prometheusService *prometheusService
appConfig *appConfig
taskConfig *taskConfig
lockConfig *lockConfig
prometheusConfig *prometheusConfig
scheduler *gocron.Scheduler
updateService *updateService
eventService *eventService
actionService *actionService
actionInvocationService *actionInvocationService
webhookService *webhookService
lockService lockService
prometheusService *prometheusService
appConfig *appConfig
taskConfig *taskConfig
lockConfig *lockConfig
prometheusConfig *prometheusConfig
scheduler *gocron.Scheduler
}
const (
taskLockNameUpdatesCleanStale = "updates_clean_stale"
taskLockNameEventsCleanStale = "events_clean_stale"
taskLockNameActionsEnqueue = "actions_enqueue"
taskLockNameActionsInvoke = "actions_invoke"
taskLockNameActionsCleanStale = "actions_clean_stale"
taskLockNamePrometheusUpdate = "prometheus_update"
)
func newTaskService(u *updateService, e *eventService, w *webhookService, l lockService, p *prometheusService, ac *appConfig, tc *taskConfig, lc *lockConfig, pc *prometheusConfig) *taskService {
var (
initialTasksStartDelay = time.Now().Add(10 * time.Second)
)
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 {
location, err := time.LoadLocation(ac.timeZone)
if err != nil {
@ -57,22 +66,27 @@ func newTaskService(u *updateService, e *eventService, w *webhookService, l lock
}
return &taskService{
updateService: u,
eventService: e,
webhookService: w,
lockService: l,
prometheusService: p,
appConfig: ac,
taskConfig: tc,
lockConfig: lc,
prometheusConfig: pc,
scheduler: scheduler,
updateService: u,
eventService: e,
actionService: a,
actionInvocationService: ai,
webhookService: w,
lockService: l,
prometheusService: p,
appConfig: ac,
taskConfig: tc,
lockConfig: lc,
prometheusConfig: pc,
scheduler: scheduler,
}
}
func (s *taskService) init() {
s.configureCleanupStaleUpdatesTask()
s.configureCleanupStaleEventsTask()
s.configureActionsEnqueueTask()
s.configureActionsInvokeTask()
s.configureCleanupStaleActionsTask()
s.configurePrometheusRefreshTask()
}
@ -92,9 +106,8 @@ func (s *taskService) configureCleanupStaleUpdatesTask() {
if !s.taskConfig.updateCleanStaleEnabled {
return
}
initialDelay := time.Now().Add(10 * time.Second)
_, err := s.scheduler.Every(s.taskConfig.updateCleanStaleInterval).
StartAt(initialDelay).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNameUpdatesCleanStale
// distributed lock handled via gocron-redis-lock for tasks
@ -127,7 +140,7 @@ func (s *taskService) configureCleanupStaleUpdatesTask() {
if c > 0 {
zap.L().Sugar().Infof("Cleaned up '%d' stale updates", c)
} else {
zap.L().Info("No stale updates found to clean up")
zap.L().Debug("No stale updates found to clean up")
}
})
@ -141,9 +154,8 @@ func (s *taskService) configureCleanupStaleEventsTask() {
return
}
initialDelay := time.Now().Add(5 * time.Second)
_, err := s.scheduler.Every(s.taskConfig.eventCleanStaleInterval).
StartAt(initialDelay).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNameEventsCleanStale
// distributed lock handled via gocron-redis-lock for tasks
@ -168,7 +180,7 @@ func (s *taskService) configureCleanupStaleEventsTask() {
var err error
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())
return
}
@ -176,7 +188,7 @@ func (s *taskService) configureCleanupStaleEventsTask() {
if c > 0 {
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
} else {
zap.L().Info("No stale events found to clean up")
zap.L().Debug("No stale events found to clean up")
}
})
@ -185,14 +197,137 @@ func (s *taskService) configureCleanupStaleEventsTask() {
}
}
func (s *taskService) configureActionsEnqueueTask() {
if !s.taskConfig.actionsEnqueueEnabled {
return
}
_, err := s.scheduler.Every(s.taskConfig.actionsEnqueueInterval).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNameActionsEnqueue
// 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)
}
if err := s.actionInvocationService.enqueue(s.taskConfig.actionsEnqueueBatchSize); err != nil {
zap.L().Sugar().Errorf("Could enqueue actions. Reason: %s", err.Error())
}
})
if err != nil {
zap.L().Sugar().Fatalf("Could not create task for enqueueing actions. Reason: %s", err.Error())
}
}
func (s *taskService) configureActionsInvokeTask() {
if !s.taskConfig.actionsInvokeEnabled {
return
}
_, err := s.scheduler.Every(s.taskConfig.actionsInvokeInterval).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNameActionsInvoke
// 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)
}
if err := s.actionInvocationService.invoke(s.taskConfig.actionsInvokeBatchSize, s.taskConfig.actionsInvokeMaxRetries); err != nil {
zap.L().Sugar().Errorf("Could invoke actions. Reason: %s", err.Error())
}
})
if err != nil {
zap.L().Sugar().Fatalf("Could not create task for invoking actions. Reason: %s", err.Error())
}
}
func (s *taskService) configureCleanupStaleActionsTask() {
if !s.taskConfig.actionsCleanStaleEnabled {
return
}
_, err := s.scheduler.Every(s.taskConfig.actionsCleanStaleInterval).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNameActionsCleanStale
// 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)
}
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")
}
})
if err != nil {
zap.L().Sugar().Fatalf("Could not create task for cleaning stale actions. Reason: %s", err.Error())
}
}
func (s *taskService) configurePrometheusRefreshTask() {
if !s.prometheusConfig.enabled {
return
}
initialDelay := time.Now().Add(10 * time.Second)
_, err := s.scheduler.Every(s.taskConfig.prometheusRefreshInterval).
StartAt(initialDelay).
StartAt(initialTasksStartDelay).
Do(func() {
resource := taskLockNamePrometheusUpdate
// distributed lock handled via gocron-redis-lock for tasks
@ -223,20 +358,12 @@ func (s *taskService) configurePrometheusRefreshTask() {
var ackTotal int64
for _, update := range updates {
var updateState float64
if api.UpdateStatePending.Value() == update.State {
pendingTotal += 1
updateState = 0
} else if api.UpdateStateIgnored.Value() == update.State {
ignoredTotal += 1
updateState = 2
} else if api.UpdateStateApproved.Value() == update.State {
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())
}
}
@ -265,6 +392,14 @@ func (s *taskService) configurePrometheusRefreshTask() {
if eventsError = s.prometheusService.setGaugeNoLabels(metricEvents, float64(eventsTotal)); eventsError != nil {
zap.L().Sugar().Errorf("Could not refresh events prometheus metric. Reason: %s", eventsError.Error())
}
// actions
var actionsTotal int64
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())
}
})
if err != nil {

View file

@ -8,16 +8,14 @@ import (
)
type updateService struct {
repo updateRepository
eventService *eventService
prometheusService *prometheusService
repo updateRepository
eventService *eventService
}
func newUpdateService(r updateRepository, e *eventService, p *prometheusService) *updateService {
func newUpdateService(r updateRepository, e *eventService) *updateService {
return &updateService{
repo: r,
eventService: e,
prometheusService: p,
repo: r,
eventService: e,
}
}
@ -118,10 +116,6 @@ func (s *updateService) delete(id string) error {
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)
return nil
}

View file

@ -10,14 +10,12 @@ import (
type webhookService struct {
repo WebhookRepository
webhookConfig *webhookConfig
eventService *eventService
}
func newWebhookService(r WebhookRepository, c *webhookConfig, e *eventService) *webhookService {
func newWebhookService(r WebhookRepository, c *webhookConfig) *webhookService {
return &webhookService{
repo: r,
webhookConfig: c,
eventService: e,
}
}
@ -42,16 +40,15 @@ func (s *webhookService) create(label string, t api.WebhookType, ignoreHost bool
var err error
var token string
var e *Webhook
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
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 {
return nil, err
} else {
s.eventService.createWebhookCreated(e)
zap.L().Sugar().Info("Created webhook")
return e, nil
}
@ -69,12 +66,10 @@ func (s *webhookService) updateLabel(id string, label string) (*Webhook, error)
return nil, err
}
old := e
if e, err = s.repo.updateLabel(id, label); err != nil {
return nil, err
}
s.eventService.createWebhookUpdated(old, e)
zap.L().Sugar().Infof("Modified webhook '%v'", id)
return e, nil
}
@ -91,12 +86,10 @@ func (s *webhookService) updateIgnoreHost(id string, ignoreHost bool) (*Webhook,
return nil, err
}
old := e
if e, err = s.repo.updateIgnoreHost(id, ignoreHost); err != nil {
return nil, err
}
s.eventService.createWebhookUpdated(old, e)
zap.L().Sugar().Infof("Modified webhook '%v'", id)
return e, nil
}
@ -115,7 +108,6 @@ func (s *webhookService) delete(id string) error {
return err
}
s.eventService.createWebhookDeleted(e)
zap.L().Sugar().Infof("Deleted webhook '%v'", id)
return nil

84
util/encryption.go Normal file
View file

@ -0,0 +1,84 @@
package util
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
)
func ConvertToBase64(input []byte) (string, error) {
if input == nil {
return "", errors.New("cannot convert to base64 with nil input")
}
return base64.URLEncoding.EncodeToString(input), nil
}
func ConvertFromBase64(input string) ([]byte, error) {
if input == "" {
return nil, errors.New("cannot convert from base64 with blank input")
}
return base64.URLEncoding.DecodeString(input)
}
// EncryptAndEncode encrypts an input with a secret key and returns the base64 encoded and encrypted string
func EncryptAndEncode(input string, secretKey string) (string, error) {
var err error
var block cipher.Block
if block, err = aes.NewCipher([]byte(secretKey)); err != nil {
return "", fmt.Errorf("cannot create cipher for encrypting: %w", err)
}
var gcm cipher.AEAD
if gcm, err = cipher.NewGCM(block); err != nil {
return "", fmt.Errorf("cannot create gcm for encrypting: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err = rand.Read(nonce); err != nil {
return "", fmt.Errorf("cannot read nonce for encrypting: %w", err)
}
ciphertext := gcm.Seal(nonce, nonce, []byte(input), nil)
var encoded string
if encoded, err = ConvertToBase64(ciphertext); err != nil {
return "", fmt.Errorf("cannot encode encrypted input: %w", err)
}
return encoded, nil
}
// DecryptAndDecode decodes and decrypts a given base64 string and returns the decrypted plain text
func DecryptAndDecode(base64Encoded string, secretKey string) (string, error) {
var decoded []byte
var err error
if decoded, err = ConvertFromBase64(base64Encoded); err != nil {
return "", fmt.Errorf("cannot decode encoded input: %w", err)
}
decodedCipherText := string(decoded)
var block cipher.Block
if block, err = aes.NewCipher([]byte(secretKey)); err != nil {
return "", fmt.Errorf("cannot create cipher for decrypting: %w", err)
}
var gcm cipher.AEAD
if gcm, err = cipher.NewGCM(block); err != nil {
return "", fmt.Errorf("cannot create gcm for decrypting: %w", err)
}
nonceSize := gcm.NonceSize()
nonce, decodedCipherText := decodedCipherText[:nonceSize], decodedCipherText[nonceSize:]
var plaintext []byte
if plaintext, err = gcm.Open(nil, []byte(nonce), []byte(decodedCipherText), nil); err != nil {
return "", fmt.Errorf("cannot decrypt: %w", err)
}
return string(plaintext), nil
}

37
util/encryption_test.go Normal file
View file

@ -0,0 +1,37 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestEncryptsAndDecrypts(t *testing.T) {
a := assert.New(t)
testSecret := "mysecretpassword"
testText := "the super secret text"
encrypted, err := EncryptAndEncode(testText, testSecret)
a.Nil(err)
a.NotEmpty(encrypted)
a.NotEqual(testText, encrypted)
decrypted, err := DecryptAndDecode(encrypted, testSecret)
a.Nil(err)
a.Equal(testText, decrypted)
}
func TestEncodeAndDecode(t *testing.T) {
a := assert.New(t)
s := "my to be encoded value"
encoded, err := ConvertToBase64([]byte(s))
a.Nil(err)
a.NotEmpty(encoded)
decoded, err := ConvertFromBase64(encoded)
a.Nil(err)
a.Equal(s, string(decoded))
}

8
util/json.go Normal file
View file

@ -0,0 +1,8 @@
package util
import "encoding/json"
// UnmarshalGenericJSON unmarshal JSON into given generic type T
func UnmarshalGenericJSON[T any](b []byte) (v T, err error) {
return v, json.Unmarshal(b, &v)
}

View file

@ -6,6 +6,7 @@ import (
"strings"
)
// FindInSlice finds value in a slice
func FindInSlice(slice []string, val string) bool {
for _, item := range slice {
if item == val {
@ -15,6 +16,7 @@ func FindInSlice(slice []string, val string) bool {
return false
}
// ValuesString concatenate all values of a map split by comma
func ValuesString(m map[string]string) string {
values := make([]string, 0, len(m))
for _, v := range m {
@ -26,6 +28,7 @@ func ValuesString(m map[string]string) string {
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
// ToSnakeCase converts string to snake case
func ToSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
@ -38,6 +41,7 @@ const (
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)
// RandomString generates a random string of length n
func RandomString(n int) string {
if n <= 0 {
return ""
@ -51,3 +55,13 @@ func RandomString(n int) string {
}
return string(b)
}
// ExtractBetween extracts all occurrences of a string within left and right delimiter, first inner array item is with delimiters, second one without
func ExtractBetween(str string, leftDelimiter string, rightDelimiter string) [][]string {
if str == "" || leftDelimiter == "" || rightDelimiter == "" {
return make([][]string, 0)
}
rx := regexp.MustCompile(`(?s)` + regexp.QuoteMeta(leftDelimiter) + `(.*?)` + regexp.QuoteMeta(rightDelimiter))
return rx.FindAllStringSubmatch(str, -1)
}

View file

@ -35,3 +35,26 @@ func TestExtractValuesFromString(t *testing.T) {
a.Contains(valuesString, "val1")
a.Contains(valuesString, "val2")
}
func TestExtractBetweenEmpty(t *testing.T) {
a := assert.New(t)
a.Equal(0, len(ExtractBetween("", "", "")))
a.Equal(0, len(ExtractBetween("test", "", "")))
a.Equal(0, len(ExtractBetween("test", "test", "")))
}
func TestExtractBetweenVars(t *testing.T) {
a := assert.New(t)
left := "<VAR>"
right := "</VAR>"
//str := "A new update arrived on <VAR>HOST</VAR> for <VAR>APPLICATION</VAR>. Its version is <VAR>VERSION</VAR>"
str := "<VAR>MY_VAR</VAR> A new update arrived on <VA>HOST</VAR> for for <VAR>APPLICATION</VAR> (<VAR>MY_SECOND_VAR</VAR>) ... some random other tag <SECRET>MY_SECRET</SECRET>"
matches := ExtractBetween(str, left, right)
a.Equal(true, len(matches) > 0)
a.Equal("MY_VAR", matches[0][1])
a.Equal("APPLICATION", matches[1][1])
a.Equal("MY_SECOND_VAR", matches[2][1])
}