From b6438b59d40ddf7e92f6881dd1f7e2be40b2ffc2 Mon Sep 17 00:00:00 2001 From: Varakh Date: Sat, 20 Jan 2024 19:13:11 +0100 Subject: [PATCH] feat(actions): Add basic functionality for actions and secrets (with proper asynchronous enqueue and dequeue mechanism) --- .forgejo/workflows/release.yaml | 4 +- CHANGELOG.md | 11 +- README.md | 228 ++-- _doc/DEPLOYMENT.md | 4 + _doc/api.yaml | 1587 ++++++++++++++++++++++- _doc/upda-grafana-dashboard.json | 962 +++++++------- _doc/updaserver.postman_collection.json | 1492 +++++++++++++++++++-- api/constants.go | 55 +- api/dto.go | 251 +++- go.mod | 7 +- go.sum | 127 +- server/api_handler_action.go | 229 ++++ server/api_handler_action_invocation.go | 102 ++ server/api_handler_event.go | 11 +- server/api_handler_secret.go | 97 ++ server/api_handler_update.go | 2 +- server/api_handler_webhook.go | 16 +- server/app.go | 42 +- server/constants_app.go | 2 +- server/constants_env.go | 27 +- server/constants_prometheus.go | 6 +- server/dto.go | 15 + server/entity.go | 141 +- server/environment.go | 67 +- server/errors.go | 11 +- server/repository_action.go | 335 +++++ server/repository_action_invocation.go | 265 ++++ server/repository_event.go | 67 +- server/repository_secret.go | 131 ++ server/repository_update.go | 2 +- server/repository_webhook.go | 2 +- server/service_action.go | 260 ++++ server/service_action_invocation.go | 402 ++++++ server/service_event.go | 141 +- server/service_prometheus.go | 2 +- server/service_secret.go | 108 ++ server/service_task.go | 211 ++- server/service_update.go | 16 +- server/service_webhook.go | 12 +- util/encryption.go | 84 ++ util/encryption_test.go | 37 + util/json.go | 8 + util/string.go | 14 + util/string_test.go | 23 + 44 files changed, 6601 insertions(+), 1015 deletions(-) create mode 100644 server/api_handler_action.go create mode 100644 server/api_handler_action_invocation.go create mode 100644 server/api_handler_secret.go create mode 100644 server/dto.go create mode 100644 server/repository_action.go create mode 100644 server/repository_action_invocation.go create mode 100644 server/repository_secret.go create mode 100644 server/service_action.go create mode 100644 server/service_action_invocation.go create mode 100644 server/service_secret.go create mode 100644 util/encryption.go create mode 100644 util/encryption_test.go create mode 100644 util/json.go diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml index 65c8e2f..28695cc 100644 --- a/.forgejo/workflows/release.yaml +++ b/.forgejo/workflows/release.yaml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0457a42..6f62fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,17 @@ 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 +* Updated build to use Go 1.22 ## [1.0.3] - 2024/01/21 diff --git a/README.md b/README.md index 7e703f0..5753ae8 100644 --- a/README.md +++ b/README.md @@ -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 `/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://:@localhost:6379/`. | | -| | | | -| `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 `/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://:@localhost:6379/`. | | +| | | | +| `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/` @@ -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_KEY` 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 `VARIABLE_NAME` syntax and any +occurrence is replaced before invocation as well. + +| Variable name | Description | +|:-------------------------|:--------------------------------------------------| +| `APPLICATION` | The update's application name invoking the action | +| `PROVIDER` | The update's provider name invoking the action | +| `HOST` | The update's host invoking the action | +| `VERSION` | 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/`, where `` +can also be provided as secret: `gotify://gotify.example.com:443/GOTIFY_TOKEN`. + +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/GOTIFY_TOKEN/?title=Great+News+On+Upda" + ], + "body": "A new update arrived on HOST for APPLICATION. Its version is VERSION." + } +} +``` + ### 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 diff --git a/_doc/DEPLOYMENT.md b/_doc/DEPLOYMENT.md index 5117d78..3808c89 100644 --- a/_doc/DEPLOYMENT.md +++ b/_doc/DEPLOYMENT.md @@ -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 diff --git a/_doc/api.yaml b/_doc/api.yaml index f62c4ed..3355957 100644 --- a/_doc/api.yaml +++ b/_doc/api.yaml @@ -5,7 +5,7 @@ info: license: name: GPLv3 url: https://www.gnu.org/licenses/gpl-3.0.en.html - version: 1.1.0 + version: 2.0.0 externalDocs: description: Find out more about the project url: https://git.myservermanager.com/varakh/upda @@ -18,6 +18,12 @@ tags: description: Webhooks endpoints - name: events description: Events endpoints + - name: secrets + description: Secrets endpoints + - name: actions + description: Actions endpoints + - name: action-invocations + description: Action invocations endpoints - name: application description: Application related endpoints - name: auth @@ -335,7 +341,7 @@ paths: description: Creates webhook operationId: createWebhook requestBody: - description: Creates an announcement + description: Creates a webhook content: application/json: schema: @@ -406,7 +412,7 @@ paths: required: false schema: type: string - default: desc + default: asc enum: - asc - desc @@ -416,7 +422,7 @@ paths: required: false schema: type: string - default: updated_at + default: label enum: - id - label @@ -519,6 +525,58 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - webhooks + security: + - basicAuth: [ ] + summary: Finds a webhook by ID + description: Finds webhook by ID + operationId: findWebhookById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' delete: tags: - webhooks @@ -769,12 +827,64 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' '/events/{id}': + get: + tags: + - events + security: + - basicAuth: [ ] + summary: Finds an event by ID + description: Finds event by ID + operationId: findEventById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/EventSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' delete: tags: - events security: - basicAuth: [ ] - summary: Deletes a event by ID + summary: Deletes an event by ID description: 'Deletes a event by ID' operationId: deleteEventById parameters: @@ -817,6 +927,1156 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + /actions: + post: + tags: + - actions + security: + - basicAuth: [ ] + summary: Creates action + description: Creates action + operationId: createAction + requestBody: + description: Creates an action + content: + application/json: + schema: + $ref: '#/components/schemas/CreateActionRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - actions + security: + - basicAuth: [ ] + summary: Finds actions + description: Finds actions + operationId: findActions + parameters: + - name: page + in: query + description: the page + required: false + schema: + type: number + default: 1 + - name: pageSize + in: query + description: the page size + required: false + schema: + type: number + default: 5 + - name: order + in: query + description: the order + required: false + schema: + type: string + default: asc + enum: + - asc + - desc + - name: orderBy + in: query + description: the order by + required: false + schema: + type: string + default: label + enum: + - id + - label + - type + - created_at + - updated_at + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionPageResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/label': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's label by ID + description: Modifies action's label by ID + operationId: patchActionLabelById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionLabelRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/type': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's type by ID + description: Modifies action's type by ID + operationId: patchActionTypeById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionTypeRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/match-event': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's matchEvent by ID + description: Modifies action's matchEvent by ID + operationId: patchActionMatchEventById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionMatchEventRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/match-host': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's matchHost by ID + description: Modifies action's matchHost by ID + operationId: patchActionMatchHostById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionMatchHostRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/match-application': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's matchApplication by ID + description: Modifies action's matchApplication by ID + operationId: patchActionMatchApplicationById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionMatchApplicationRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/match-provider': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's matchProvider by ID + description: Modifies action's matchProvider by ID + operationId: patchActionMatchProviderById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionMatchProviderRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/payload': + patch: + tags: + - actions + security: + - basicAuth: [ ] + summary: Modifies action's payload by ID + description: Modifies action's payload by ID + operationId: patchActionPayloadById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies an action + content: + application/json: + schema: + $ref: '#/components/schemas/ModifyActionPayloadRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}/test': + post: + tags: + - actions + security: + - basicAuth: [ ] + summary: Tests action by ID + description: Tests action by ID + operationId: postActionTestById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Tests an action + content: + application/json: + schema: + $ref: '#/components/schemas/TestActionRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionTestSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/actions/{id}': + get: + tags: + - actions + security: + - basicAuth: [ ] + summary: Finds an action by ID + description: Finds action by ID + operationId: findActionById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - actions + security: + - basicAuth: [ ] + summary: Deletes an action by ID + description: 'Deletes an action by ID' + operationId: deleteActionById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '204': + description: Successful operation + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /action-invocations: + get: + tags: + - action-invocations + security: + - basicAuth: [ ] + summary: Finds action invocations + description: Finds action invocations + operationId: findActionInvocations + parameters: + - name: page + in: query + description: the page + required: false + schema: + type: number + default: 1 + - name: pageSize + in: query + description: the page size + required: false + schema: + type: number + default: 5 + - name: order + in: query + description: the order + required: false + schema: + type: string + default: desc + enum: + - asc + - desc + - name: orderBy + in: query + description: the order by + required: false + schema: + type: string + default: created_at + enum: + - id + - state + - retry_count + - created_at + - updated_at + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionInvocationPageResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/action-invocations/{id}': + get: + tags: + - action-invocations + security: + - basicAuth: [ ] + summary: Finds an action invocation by ID + description: Finds action invocation by ID + operationId: findActionInvocationById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ActionInvocationSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - action-invocations + security: + - basicAuth: [ ] + summary: Deletes an action invocation by ID + description: 'Deletes an action invocation by ID' + operationId: deleteActionInvocationById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '204': + description: Successful operation + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /secrets: + post: + tags: + - secrets + security: + - basicAuth: [ ] + summary: Creates secret + description: Creates secret + operationId: createSecret + requestBody: + description: Creates a secret + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSecretRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SecretSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + get: + tags: + - secrets + security: + - basicAuth: [ ] + summary: Finds secrets + description: Finds secrets + operationId: findSecrets + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SecretPageResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/secrets/{id}': + get: + tags: + - secrets + security: + - basicAuth: [ ] + summary: Finds a secret by ID + description: Finds secret by ID + operationId: findSecretById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SecretSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - secrets + security: + - basicAuth: [ ] + summary: Deletes a secret by ID + description: 'Deletes a secret by ID' + operationId: deleteSecretById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + responses: + '204': + description: Successful operation + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '/secrets/{id}/value': + patch: + tags: + - secrets + security: + - basicAuth: [ ] + summary: Modifies secret's value by ID + description: Modifies secret's value by ID + operationId: patchSecretValueById + parameters: + - name: id + in: path + description: the id + required: true + schema: + type: string + requestBody: + description: Modifies a secret + content: + application/json: + schema: + $ref: '#/components/schemas/ModifySecretValueRequest' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/SecretSingleResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /login: get: tags: @@ -1053,6 +2313,13 @@ components: - desc hasNext: type: boolean + EventSingleResponse: + type: object + properties: + data: + type: object + allOf: + - $ref: '#/components/schemas/EventResponse' EventResponse: type: object properties: @@ -1067,14 +2334,6 @@ components: - update_updated_state_approved - update_updated_state_ignored - update_deleted - - webhook_created - - webhook_updated_label - - webhook_updated_ignore_host - - webhook_deleted - state: - type: string - enum: - - created createdAt: type: string updatedAt: @@ -1087,9 +2346,6 @@ components: - $ref: '#/components/schemas/EventPayloadUpdateCreatedResponse' - $ref: '#/components/schemas/EventPayloadUpdateUpdatedResponse' - $ref: '#/components/schemas/EventPayloadUpdateDeletedResponse' - - $ref: '#/components/schemas/EventPayloadWebhookCreatedResponse' - - $ref: '#/components/schemas/EventPayloadWebhookUpdatedResponse' - - $ref: '#/components/schemas/EventPayloadWebhookDeletedResponse' EventPayloadUpdateCreatedResponse: type: object properties: @@ -1147,7 +2403,7 @@ components: type: string version: type: string - EventPayloadWebhookCreatedResponse: + ActionResponse: type: object properties: id: @@ -1157,40 +2413,173 @@ components: type: type: string enum: - - generic - - diun - ignoreHost: + - shoutrrr + matchEvent: + type: string + nullable: true + matchHost: + type: string + nullable: true + matchApplication: + type: string + nullable: true + matchProvider: + type: string + nullable: true + payload: + type: object + createdAt: + type: string + updatedAt: + type: string + ActionSingleResponse: + type: object + properties: + data: + type: object + allOf: + - $ref: '#/components/schemas/ActionResponse' + ActionPageResponse: + type: object + properties: + data: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/ActionResponse' + page: + type: number + pageSize: + type: number + orderBy: + type: string + enum: + - id + - created_at + - updated_at + - label + - type + order: + type: string + enum: + - asc + - desc + totalElements: + type: number + totalPages: + type: number + ActionTestResponse: + type: object + properties: + success: type: boolean - EventPayloadWebhookUpdatedResponse: + message: + type: string + description: Blank in case of success + ActionTestSingleResponse: + type: object + properties: + data: + type: object + allOf: + - $ref: '#/components/schemas/ActionTestResponse' + ActionInvocationResponse: type: object properties: id: type: string - labelPrior: - type: string - label: - type: string - type: + retryCount: + type: number + state: type: string enum: - - generic - - diun - ignoreHostPrior: - type: boolean - ignoreHost: - type: boolean - EventPayloadWebhookDeletedResponse: + - created + - running + - retrying + - error + - success + message: + type: string + nullable: true + actionId: + type: string + eventId: + type: string + createdAt: + type: string + updatedAt: + type: string + ActionInvocationSingleResponse: type: object properties: - label: + data: + type: object + allOf: + - $ref: '#/components/schemas/ActionInvocationResponse' + ActionInvocationPageResponse: + type: object + properties: + data: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/ActionInvocationResponse' + page: + type: number + pageSize: + type: number + orderBy: + type: string + enum: + - id + - created_at + - updated_at + - state + - retry_count + order: + type: string + enum: + - asc + - desc + totalElements: + type: number + totalPages: + type: number + SecretResponse: + type: object + properties: + id: type: string - type: + key: type: string - enum: - - generic - - diun - ignoreHost: - type: boolean + value: + type: string + nullable: true + createdAt: + type: string + updatedAt: + type: string + SecretSingleResponse: + type: object + properties: + data: + type: object + allOf: + - $ref: '#/components/schemas/SecretResponse' + SecretPageResponse: + type: object + properties: + data: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/SecretResponse' # requests ModifyUpdateStateRequest: @@ -1306,3 +2695,121 @@ components: type: string ctn_status: type: string + CreateActionRequest: + type: object + required: + - label + - type + - payload + properties: + label: + type: string + type: + type: string + enum: + - shoutrrr + matchEvent: + type: string + matchHost: + type: string + matchApplication: + type: string + matchProvider: + type: string + payload: + type: object + anyOf: + - $ref: '#/components/schemas/CreateActionPayloadRequestShoutrrr' + CreateActionPayloadRequestShoutrrr: + type: object + required: + - urls + - body + properties: + urls: + type: array + items: + type: string + body: + type: string + TestActionRequest: + type: object + required: + - application + - provider + - host + - version + properties: + application: + type: string + provider: + type: string + host: + type: string + version: + type: string + CreateSecretRequest: + type: object + required: + - key + - value + properties: + key: + type: string + value: + type: string + ModifySecretValueRequest: + type: object + required: + - value + properties: + value: + type: string + ModifyActionLabelRequest: + type: object + required: + - label + properties: + label: + type: string + ModifyActionTypeRequest: + type: object + required: + - type + properties: + type: + type: string + enum: + - shoutrrr + ModifyActionMatchEventRequest: + type: object + properties: + matchEvent: + type: string + nullable: true + ModifyActionMatchHostRequest: + type: object + properties: + matchHost: + type: string + nullable: true + ModifyActionMatchApplicationRequest: + type: object + properties: + matchApplication: + type: string + nullable: true + ModifyActionMatchProviderRequest: + type: object + properties: + matchProvider: + type: string + nullable: true + ModifyActionPayloadRequest: + type: object + required: + - payload + properties: + payload: + type: object + nullable: false \ No newline at end of file diff --git a/_doc/upda-grafana-dashboard.json b/_doc/upda-grafana-dashboard.json index b6abe69..e1510e6 100644 --- a/_doc/upda-grafana-dashboard.json +++ b/_doc/upda-grafana-dashboard.json @@ -21,7 +21,7 @@ "type": "grafana", "id": "grafana", "name": "Grafana", - "version": "10.1.5" + "version": "10.4.0" }, { "type": "datasource", @@ -111,17 +111,17 @@ "overrides": [] }, "gridPos": { - "h": 4, - "w": 9, + "h": 5, + "w": 8, "x": 0, "y": 1 }, "id": 37, "options": { "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", + "graphMode": "none", + "justifyMode": "center", + "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" @@ -129,9 +129,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -188,97 +190,6 @@ "title": "Updates", "type": "stat" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "mappings": [ - { - "options": { - "0": { - "color": "blue", - "index": 1, - "text": "pending" - }, - "1": { - "color": "green", - "index": 2, - "text": "approved" - }, - "2": { - "color": "red", - "index": 3, - "text": "ignored" - }, - "-1": { - "color": "transparent", - "index": 0, - "text": "deleted" - } - }, - "type": "value" - } - ], - "max": 2, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "transparent", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 15, - "x": 9, - "y": 1 - }, - "id": 26, - "links": [], - "options": { - "colorMode": "background_solid", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "textMode": "value_and_name" - }, - "pluginVersion": "10.1.5", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "exemplar": false, - "expr": "upda_updates{job=\"$job\", instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{application}} {{host}} {{provider}}", - "range": true, - "refId": "A" - } - ], - "title": "Updates Details", - "type": "stat" - }, { "datasource": { "type": "prometheus", @@ -304,10 +215,10 @@ "overrides": [] }, "gridPos": { - "h": 4, + "h": 5, "w": 2, - "x": 0, - "y": 5 + "x": 8, + "y": 1 }, "id": 38, "options": { @@ -322,9 +233,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -367,10 +280,10 @@ "overrides": [] }, "gridPos": { - "h": 4, + "h": 5, "w": 2, - "x": 2, - "y": 5 + "x": 10, + "y": 1 }, "id": 39, "options": { @@ -385,9 +298,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -405,13 +320,78 @@ "title": "Webhooks", "type": "stat" }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 2, + "x": 12, + "y": 1 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "upda_actions{job=\"$job\", instance=~\"$instance\"}", + "instant": false, + "legendFormat": "Total", + "range": true, + "refId": "A" + } + ], + "title": "Actions", + "type": "stat" + }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 10 + "y": 6 }, "id": 34, "panels": [], @@ -447,10 +427,9 @@ "h": 8, "w": 9, "x": 0, - "y": 11 + "y": 7 }, "id": 28, - "links": [], "options": { "colorMode": "value", "graphMode": "area", @@ -463,9 +442,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -517,14 +498,15 @@ "h": 8, "w": 15, "x": 9, - "y": 11 + "y": 7 }, - "id": 40, - "links": [], + "id": 31, "options": { "displayMode": "gradient", + "maxVizHeight": 300, "minVizHeight": 10, "minVizWidth": 0, + "namePlacement": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ @@ -534,9 +516,10 @@ "values": false }, "showUnfilled": true, + "sizing": "auto", "valueMode": "color" }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -544,15 +527,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "upda_request_duration_count{job=\"$job\", instance=~\"$instance\"}", + "expr": "upda_requests_total{job=\"$job\", instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{method}} | {{path}}", + "legendFormat": "{{code}} | {{method}} | {{path}}", "range": true, "refId": "A" } ], - "title": "Request Duration Count", + "title": "Request Total", "type": "bargauge" }, { @@ -583,7 +566,7 @@ "h": 4, "w": 3, "x": 0, - "y": 19 + "y": 15 }, "id": 29, "options": { @@ -598,9 +581,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -646,7 +631,7 @@ "h": 4, "w": 3, "x": 3, - "y": 19 + "y": 15 }, "id": 27, "options": { @@ -661,9 +646,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -709,7 +696,7 @@ "h": 4, "w": 3, "x": 6, - "y": 19 + "y": 15 }, "id": 30, "options": { @@ -724,9 +711,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -777,14 +766,15 @@ "h": 8, "w": 15, "x": 9, - "y": 19 + "y": 15 }, - "id": 31, - "links": [], + "id": 40, "options": { "displayMode": "gradient", + "maxVizHeight": 300, "minVizHeight": 10, "minVizWidth": 0, + "namePlacement": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ @@ -794,9 +784,10 @@ "values": false }, "showUnfilled": true, + "sizing": "auto", "valueMode": "color" }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -804,15 +795,15 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "upda_requests_total{job=\"$job\", instance=~\"$instance\"}", + "expr": "upda_request_duration_count{job=\"$job\", instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{code}} | {{method}} | {{path}}", + "legendFormat": "{{method}} | {{path}}", "range": true, "refId": "A" } ], - "title": "Request Total", + "title": "Request Duration Count", "type": "bargauge" }, { @@ -843,7 +834,7 @@ "h": 4, "w": 3, "x": 0, - "y": 23 + "y": 19 }, "id": 33, "options": { @@ -858,9 +849,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -906,7 +899,7 @@ "h": 4, "w": 3, "x": 3, - "y": 23 + "y": 19 }, "id": 32, "options": { @@ -921,9 +914,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -969,7 +964,7 @@ "h": 4, "w": 3, "x": 6, - "y": 23 + "y": 19 }, "id": 41, "options": { @@ -984,9 +979,11 @@ "fields": "", "values": false }, - "textMode": "auto" + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "pluginVersion": "10.1.5", + "pluginVersion": "10.4.0", "targets": [ { "datasource": { @@ -1004,19 +1001,6 @@ "title": "Requests with error", "type": "stat" }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 27 - }, - "id": 42, - "panels": [], - "title": "Runtime", - "type": "row" - }, { "datasource": { "type": "prometheus", @@ -1028,6 +1012,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1077,14 +1062,127 @@ }, "overrides": [] }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 46, + "options": { + "legend": { + "calcs": [ + "last", + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "go_memstats_sys_bytes{job=\"$job\", instance=~\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "title": "Total Used Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 42, + "panels": [], + "title": "Runtime", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes", + "unitScale": true + }, + "overrides": [] + }, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 28 + "y": 32 }, "id": 43, - "links": [], "options": { "legend": { "calcs": [], @@ -1195,6 +1293,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1231,8 +1330,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1240,7 +1338,8 @@ } ] }, - "unit": "decbytes" + "unit": "decbytes", + "unitScale": true }, "overrides": [] }, @@ -1248,10 +1347,9 @@ "h": 8, "w": 12, "x": 12, - "y": 28 + "y": 32 }, "id": 44, - "links": [], "options": { "legend": { "calcs": [], @@ -1332,6 +1430,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1376,7 +1475,8 @@ } ] }, - "unit": "decbytes" + "unit": "decbytes", + "unitScale": true }, "overrides": [] }, @@ -1384,10 +1484,9 @@ "h": 8, "w": 12, "x": 0, - "y": 36 + "y": 40 }, "id": 45, - "links": [], "options": { "legend": { "calcs": [], @@ -1426,196 +1525,6 @@ "title": "Memory in Stack", "type": "timeseries" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "decbytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 36 - }, - "id": 46, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "10.1.5", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "go_memstats_sys_bytes{job=\"$job\", instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "title": "Total Used Memory", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 44 - }, - "id": 47, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "10.1.5", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "go_memstats_mallocs_total{job=\"$job\", instance=~\"$instance\"} - go_memstats_frees_total{job=\"$job\", instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "title": "Number of Live Objects", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -1628,6 +1537,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1672,7 +1582,8 @@ } ] }, - "unit": "short" + "unit": "short", + "unitScale": true }, "overrides": [] }, @@ -1680,10 +1591,9 @@ "h": 8, "w": 12, "x": 12, - "y": 44 + "y": 40 }, "id": 48, - "links": [], "options": { "legend": { "calcs": [], @@ -1719,13 +1629,13 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "description": "go_memstats_lookups_total – counts how many pointer dereferences happened. This is a counter value so you can use rate() to lookups/s.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1770,7 +1680,8 @@ } ] }, - "unit": "ops" + "unit": "short", + "unitScale": true }, "overrides": [] }, @@ -1778,10 +1689,202 @@ "h": 8, "w": 12, "x": 0, - "y": 52 + "y": 48 + }, + "id": 47, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "go_memstats_mallocs_total{job=\"$job\", instance=~\"$instance\"} - go_memstats_frees_total{job=\"$job\", instance=~\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "title": "Number of Live Objects", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "go_goroutines{job=\"$job\", instance=~\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "title": "Goroutines", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "go_memstats_lookups_total – counts how many pointer dereferences happened. This is a counter value so you can use rate() to lookups/s.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ops", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 }, "id": 49, - "links": [], "options": { "legend": { "calcs": [], @@ -1821,6 +1924,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1865,7 +1969,8 @@ } ] }, - "unit": "short" + "unit": "ms", + "unitScale": true }, "overrides": [] }, @@ -1873,10 +1978,9 @@ "h": 8, "w": 12, "x": 12, - "y": 52 + "y": 56 }, - "id": 50, - "links": [], + "id": 52, "options": { "legend": { "calcs": [], @@ -1896,13 +2000,13 @@ "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, - "expr": "go_goroutines{job=\"$job\", instance=~\"$instance\"}", + "expr": "go_gc_duration_seconds{job=\"$job\", instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], - "title": "Goroutines", + "title": "GC duration quantile", "type": "timeseries" }, { @@ -1916,6 +2020,7 @@ "mode": "palette-classic" }, "custom": { + "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", @@ -1960,7 +2065,8 @@ } ] }, - "unit": "Bps" + "unit": "Bps", + "unitScale": true }, "overrides": [] }, @@ -1968,10 +2074,9 @@ "h": 8, "w": 12, "x": 0, - "y": 60 + "y": 64 }, "id": 51, - "links": [], "options": { "legend": { "calcs": [], @@ -1999,110 +2104,19 @@ ], "title": "Rates of Allocation", "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "links": [], - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 60 - }, - "id": 52, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "10.1.5", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "expr": "go_gc_duration_seconds{job=\"$job\", instance=~\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "title": "GC duration quantile", - "type": "timeseries" } ], "refresh": "5s", - "schemaVersion": 38, - "style": "dark", + "schemaVersion": 39, "tags": [], "templating": { "list": [ { + "current": { + "selected": false, + "text": "ando", + "value": "c6248fc9-eab1-4c9b-ab39-127642bc3879" + }, "hide": 0, "includeAll": false, "label": "datasource", @@ -2165,7 +2179,7 @@ ] }, "time": { - "from": "now-6h", + "from": "now-1h", "to": "now" }, "timepicker": { @@ -2196,6 +2210,6 @@ "timezone": "", "title": "upda", "uid": "CgCw8jKZ8", - "version": 4, + "version": 16, "weekStart": "" } \ No newline at end of file diff --git a/_doc/updaserver.postman_collection.json b/_doc/updaserver.postman_collection.json index 0c85732..30c2b56 100644 --- a/_doc/updaserver.postman_collection.json +++ b/_doc/updaserver.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "cf0931c9-d395-45f4-be89-a43c1b85b925", + "_postman_id": "928eb7ad-739c-425e-9a54-97258895e542", "name": "updaserver", "description": "API specification", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" @@ -267,138 +267,6 @@ } ] }, - { - "name": "events", - "item": [ - { - "name": "/events", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var jsonData = JSON.parse(responseBody);", - "", - "if (jsonData.data && jsonData.data.content.length > 0) {", - " postman.setEnvironmentVariable(\"eventId\", jsonData.data.content[0].id);", - "} else {", - " postman.setEnvironmentVariable(\"eventId\", null);", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{basicAuthPass}}", - "type": "string" - }, - { - "key": "username", - "value": "{{basicAuthUser}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/events", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "events" - ], - "query": [ - { - "key": "size", - "value": "2", - "description": "the size", - "disabled": true - }, - { - "key": "skip", - "value": "15", - "description": "the skip", - "disabled": true - }, - { - "key": "order", - "value": "asc", - "description": "the order", - "disabled": true - }, - { - "key": "orderBy", - "value": "updated_at", - "description": "the order by", - "disabled": true - } - ] - }, - "description": "Finds updates" - }, - "response": [] - }, - { - "name": "/events/:id", - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{basicAuthPass}}", - "type": "string" - }, - { - "key": "username", - "value": "{{basicAuthUser}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/events/:id", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "events", - ":id" - ], - "variable": [ - { - "key": "id", - "value": "{{eventId}}", - "description": "(Required) the id" - } - ] - }, - "description": "Deletes an update by ID" - }, - "response": [] - } - ] - }, { "name": "webhooks", "item": [ @@ -480,6 +348,55 @@ }, "response": [] }, + { + "name": "/webhooks/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/webhooks/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "webhooks", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{webhookId}}" + } + ] + }, + "description": "Creates webhook" + }, + "response": [] + }, { "name": "/webhooks (generic)", "request": { @@ -817,6 +734,1315 @@ } ] }, + { + "name": "events", + "item": [ + { + "name": "/events", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "if (jsonData.data && jsonData.data.content.length > 0) {", + " postman.setEnvironmentVariable(\"eventId\", jsonData.data.content[0].id);", + "} else {", + " postman.setEnvironmentVariable(\"eventId\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events" + ], + "query": [ + { + "key": "size", + "value": "2", + "description": "the size", + "disabled": true + }, + { + "key": "skip", + "value": "15", + "description": "the skip", + "disabled": true + }, + { + "key": "order", + "value": "asc", + "description": "the order", + "disabled": true + }, + { + "key": "orderBy", + "value": "updated_at", + "description": "the order by", + "disabled": true + } + ] + }, + "description": "Finds updates" + }, + "response": [] + }, + { + "name": "/events/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eventId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/events/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/events/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "events", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{eventId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + } + ] + }, + { + "name": "actions", + "item": [ + { + "name": "/actions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "if (jsonData.data && jsonData.data.content.length > 0) {", + " postman.setEnvironmentVariable(\"actionId\", jsonData.data.content[0].id);", + "} else {", + " postman.setEnvironmentVariable(\"actionId\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/actions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions" + ], + "query": [ + { + "key": "state", + "value": "ignored", + "description": "the state", + "disabled": true + }, + { + "key": "state", + "value": "approved", + "description": "the state", + "disabled": true + }, + { + "key": "state", + "value": "pending", + "description": "the state", + "disabled": true + } + ] + }, + "description": "Finds updates" + }, + "response": [] + }, + { + "name": "/actions/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/actions/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"label\": \"My label\",\n \"type\": \"shoutrrr\",\n \"matchEvent\": null,\n \"matchHost\": null,\n \"matchApplication\": null,\n \"matchProvider\": null,\n \"payload\": {\n \"urls\": [\n \"gotify://myurl/GOTIFY_TOKEN/?title=Great+News+On+Upda\"\n ],\n \"body\": \"A new update arrived on HOST for APPLICATION. Its version is VERSION.\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions" + ] + }, + "description": "Creates webhook" + }, + "response": [] + }, + { + "name": "/actions/:id/label", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"label\": \"changed label\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/label", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "label" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/type", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"type\": \"shoutrrr\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/type", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "type" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/match-event", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"matchEvent\": \"update_created\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/match-event", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "match-event" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/match-host", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"matchHost\": \"global\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/match-host", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "match-host" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/match-application", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"matchApplication\": \"upda-cli\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/match-application", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "match-application" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/match-provider", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"matchProvider\": \"upda-cli\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/match-provider", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "match-provider" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/payload", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"payload\": {\n \"body\": \"A custom title new\",\n \"urls\": [\n \"slack://token-a/token-b/$MY_SECRET$\"\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/payload", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "payload" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id/test", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"application\": \"Test App\",\n \"host\": \"Test Host\",\n \"provider\": \"Test Provider\",\n \"version\": \"Test Version\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/actions/:id/test", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id", + "test" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/actions/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/actions/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "actions", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{actionId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + } + ] + }, + { + "name": "action-invocations", + "item": [ + { + "name": "/action-invocations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "if (jsonData.data && jsonData.data.content.length > 0) {", + " postman.setEnvironmentVariable(\"actionInvocationId\", jsonData.data.content[0].id);", + "} else {", + " postman.setEnvironmentVariable(\"actionInvocationId\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/action-invocations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "action-invocations" + ], + "query": [ + { + "key": "order", + "value": "asc", + "description": "the state", + "disabled": true + }, + { + "key": "orderBy", + "value": "retry_count", + "description": "the state", + "disabled": true + }, + { + "key": "pageSize", + "value": "1", + "description": "the state", + "disabled": true + }, + { + "key": "page", + "value": "4", + "disabled": true + } + ] + }, + "description": "Finds updates" + }, + "response": [] + }, + { + "name": "/action-invocations/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/action-invocations/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "action-invocations", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{actionInvocationId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/action-invocations/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/action-invocations/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "action-invocations", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{actionInvocationId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + } + ] + }, + { + "name": "secrets", + "item": [ + { + "name": "/secrets", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "", + "if (jsonData.data && jsonData.data.content.length > 0) {", + " postman.setEnvironmentVariable(\"secretId\", jsonData.data.content[0].id);", + "} else {", + " postman.setEnvironmentVariable(\"secretId\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/secrets", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "secrets" + ], + "query": [ + { + "key": "state", + "value": "ignored", + "description": "the state", + "disabled": true + }, + { + "key": "state", + "value": "approved", + "description": "the state", + "disabled": true + }, + { + "key": "state", + "value": "pending", + "description": "the state", + "disabled": true + } + ] + }, + "description": "Finds updates" + }, + "response": [] + }, + { + "name": "/secrets/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/secrets/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "secrets", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{secretId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + }, + { + "name": "/secrets", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"key\": \"GOTIFY_TOKEN\",\n \"value\": \"ThisIsASecretTokenForGotify\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/secrets", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "secrets" + ] + }, + "description": "Creates webhook" + }, + "response": [] + }, + { + "name": "/secrets/:id/value", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"value\": \"UpdatedSecret\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/secrets/:id/value", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "secrets", + ":id", + "value" + ], + "variable": [ + { + "key": "id", + "value": "{{secretId}}", + "description": "the id" + } + ] + }, + "description": "Creates webhook" + }, + "response": [] + }, + { + "name": "/secrets/:id", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{basicAuthPass}}", + "type": "string" + }, + { + "key": "username", + "value": "{{basicAuthUser}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/secrets/:id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "secrets", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{secretId}}", + "description": "(Required) the id" + } + ] + }, + "description": "Deletes an update by ID" + }, + "response": [] + } + ] + }, { "name": "/info", "request": { diff --git a/api/constants.go b/api/constants.go index c91f2b6..d3ce264 100644 --- a/api/constants.go +++ b/api/constants.go @@ -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) +} diff --git a/api/dto.go b/api/dto.go index c13e815..ea00220 100644 --- a/api/dto.go +++ b/api/dto.go @@ -25,6 +25,60 @@ 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 ModifySecretValueRequest struct { + 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"` +} + +type ModifyActionLabelRequest struct { + Label string `json:"label" binding:"required,min=1,max=255"` +} + +type ModifyActionTypeRequest struct { + Type ActionType `json:"type" binding:"required,oneof=shoutrrr"` +} + +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 ModifyActionPayloadRequest struct { + Payload interface{} `json:"payload" binding:"required"` +} + +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 +89,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 +284,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 +350,158 @@ 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"` + 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{}, 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.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 } diff --git a/go.mod b/go.mod index 5b359ce..60f6355 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module git.myservermanager.com/varakh/upda -go 1.21 +go 1.22 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 diff --git a/go.sum b/go.sum index 750e2a2..8fd97fc 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/server/api_handler_action.go b/server/api_handler_action.go new file mode 100644 index 0000000..70253c4 --- /dev/null +++ b/server/api_handler_action.go @@ -0,0 +1,229 @@ +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, + 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.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); 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.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.CreatedAt, e.UpdatedAt)) +} + +func (h *actionHandler) updateType(c *gin.Context) { + var e *Action + var err error + + var req api.ModifyActionTypeRequest + + if err = c.ShouldBindJSON(&req); err != nil { + errAbortWithValidatorPayload(c, err) + return + } + + if e, err = h.service.updateType(c.Param("id"), req.Type); 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.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.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.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.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.CreatedAt, e.UpdatedAt)) +} + +func (h *actionHandler) updatePayload(c *gin.Context) { + var e *Action + var err error + + var req api.ModifyActionPayloadRequest + + if err = c.ShouldBindJSON(&req); err != nil { + errAbortWithValidatorPayload(c, err) + return + } + + if e, err = h.service.updatePayload(c.Param("id"), 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.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) +} diff --git a/server/api_handler_action_invocation.go b/server/api_handler_action_invocation.go new file mode 100644 index 0000000..10dcf35 --- /dev/null +++ b/server/api_handler_action_invocation.go @@ -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) +} diff --git a/server/api_handler_event.go b/server/api_handler_event.go index 2eccd63..ab40562 100644 --- a/server/api_handler_event.go +++ b/server/api_handler_event.go @@ -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) diff --git a/server/api_handler_secret.go b/server/api_handler_secret.go new file mode 100644 index 0000000..51910ca --- /dev/null +++ b/server/api_handler_secret.go @@ -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) +} diff --git a/server/api_handler_update.go b/server/api_handler_update.go index 5206978..fd0987c 100644 --- a/server/api_handler_update.go +++ b/server/api_handler_update.go @@ -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 } diff --git a/server/api_handler_webhook.go b/server/api_handler_webhook.go index 2e2a0fb..9cd158b 100644 --- a/server/api_handler_webhook.go +++ b/server/api_handler_webhook.go @@ -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 } diff --git a/server/app.go b/server/app.go index 6fb3538..f83b972 100644 --- a/server/app.go +++ b/server/app.go @@ -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/type", actionHandler.updateType) + 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.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{ diff --git a/server/constants_app.go b/server/constants_app.go index c43b230..72707a8 100644 --- a/server/constants_app.go +++ b/server/constants_app.go @@ -2,5 +2,5 @@ package server const ( Name = "upda" - Version = "1.1.0" + Version = "2.0.0" ) diff --git a/server/constants_env.go b/server/constants_env.go index f6f0021..3d30f52 100644 --- a/server/constants_env.go +++ b/server/constants_env.go @@ -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" diff --git a/server/constants_prometheus.go b/server/constants_prometheus.go index 033d534..b49b369 100644 --- a/server/constants_prometheus.go +++ b/server/constants_prometheus.go @@ -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" ) diff --git a/server/dto.go b/server/dto.go new file mode 100644 index 0000000..1922886 --- /dev/null +++ b/server/dto.go @@ -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 +} diff --git a/server/entity.go b/server/entity.go index 5e7bc32..31f6bef 100644 --- a/server/entity.go +++ b/server/entity.go @@ -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,101 @@ 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"` + 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"` +} diff --git a/server/environment.go b/server/environment.go index c020211..5330d3e 100644 --- a/server/environment.go +++ b/server/environment.go @@ -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 diff --git a/server/errors.go b/server/errors.go index 70ab376..e022e9e 100644 --- a/server/errors.go +++ b/server/errors.go @@ -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")) diff --git a/server/repository_action.go b/server/repository_action.go new file mode 100644 index 0000000..363917e --- /dev/null +++ b/server/repository_action.go @@ -0,0 +1,335 @@ +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) + findAll() ([]*Action, error) + create(label string, t api.ActionType, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}) (*Action, error) + updateLabel(id string, label string) (*Action, error) + updateType(id string, t api.ActionType) (*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) + updatePayload(id string, payload interface{}) (*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) create(label string, t api.ActionType, matchEvent *string, matchHost *string, matchApplication *string, matchProvider *string, payload interface{}) (*Action, error) { + if label == "" || t == "" { + return nil, errorValidationNotBlank + } + + e := &Action{ + Label: label, + Type: t.Value(), + MatchEvent: matchEvent, + MatchHost: matchHost, + MatchApplication: matchApplication, + MatchProvider: matchProvider, + } + + 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) updatePayload(id string, payload interface{}) (*Action, error) { + if id == "" { + 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 + + 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 +} diff --git a/server/repository_action_invocation.go b/server/repository_action_invocation.go new file mode 100644 index 0000000..e07496e --- /dev/null +++ b/server/repository_action_invocation.go @@ -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)) + } +} diff --git a/server/repository_event.go b/server/repository_event.go index a2e1139..66842d6 100644 --- a/server/repository_event.go +++ b/server/repository_event.go @@ -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 { diff --git a/server/repository_secret.go b/server/repository_secret.go new file mode 100644 index 0000000..420a58d --- /dev/null +++ b/server/repository_secret.go @@ -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 +} diff --git a/server/repository_update.go b/server/repository_update.go index c7fdc32..2749809 100644 --- a/server/repository_update.go +++ b/server/repository_update.go @@ -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 { diff --git a/server/repository_webhook.go b/server/repository_webhook.go index 8c6e248..d84e7a9 100644 --- a/server/repository_webhook.go +++ b/server/repository_webhook.go @@ -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 { diff --git a/server/service_action.go b/server/service_action.go new file mode 100644 index 0000000..9379f59 --- /dev/null +++ b/server/service_action.go @@ -0,0 +1,260 @@ +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{}) (*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); 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) updateType(id string, t api.ActionType) (*Action, error) { + if id == "" || t == "" { + 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.updateType(id, t); 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) updatePayload(id string, payload interface{}) (*Action, error) { + if id == "" { + 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(api.ActionType(e.Type), payload); !isValid { + return nil, newServiceError(IllegalArgument, validationErr) + } + + if e, err = s.repo.updatePayload(id, payload); 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() +} diff --git a/server/service_action_invocation.go b/server/service_action_invocation.go new file mode 100644 index 0000000..e117caf --- /dev/null +++ b/server/service_action_invocation.go @@ -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.getAll(); 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, "", "") + 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, "APPLICATION", eventPayloadInfo.Application) + str = strings.ReplaceAll(str, "PROVIDER", eventPayloadInfo.Provider) + str = strings.ReplaceAll(str, "HOST", eventPayloadInfo.Host) + str = strings.ReplaceAll(str, "VERSION", 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...) +} diff --git a/server/service_event.go b/server/service_event.go index 675fc9d..0af848e 100644 --- a/server/service_event.go +++ b/server/service_event.go @@ -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")) +} diff --git a/server/service_prometheus.go b/server/service_prometheus.go index 83154b8..aa50609 100644 --- a/server/service_prometheus.go +++ b/server/service_prometheus.go @@ -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) diff --git a/server/service_secret.go b/server/service_secret.go new file mode 100644 index 0000000..2f2c6d9 --- /dev/null +++ b/server/service_secret.go @@ -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 +} diff --git a/server/service_task.go b/server/service_task.go index dd0b33c..e225cf2 100644 --- a/server/service_task.go +++ b/server/service_task.go @@ -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 { diff --git a/server/service_update.go b/server/service_update.go index 93b0453..1798202 100644 --- a/server/service_update.go +++ b/server/service_update.go @@ -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 } diff --git a/server/service_webhook.go b/server/service_webhook.go index 47b0a34..a052158 100644 --- a/server/service_webhook.go +++ b/server/service_webhook.go @@ -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 diff --git a/util/encryption.go b/util/encryption.go new file mode 100644 index 0000000..cb73cbb --- /dev/null +++ b/util/encryption.go @@ -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 +} diff --git a/util/encryption_test.go b/util/encryption_test.go new file mode 100644 index 0000000..2d07e48 --- /dev/null +++ b/util/encryption_test.go @@ -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)) + +} diff --git a/util/json.go b/util/json.go new file mode 100644 index 0000000..a08a97d --- /dev/null +++ b/util/json.go @@ -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) +} diff --git a/util/string.go b/util/string.go index 9448485..c17628b 100644 --- a/util/string.go +++ b/util/string.go @@ -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< 0) + a.Equal("MY_VAR", matches[0][1]) + a.Equal("APPLICATION", matches[1][1]) + a.Equal("MY_SECOND_VAR", matches[2][1]) +}