This commit is contained in:
commit
d12db38a73
50 changed files with 8030 additions and 0 deletions
100
.editorconfig
Normal file
100
.editorconfig
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = false
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 4
|
||||||
|
ij_continuation_indent_size = 8
|
||||||
|
ij_formatter_off_tag = @formatter:off
|
||||||
|
ij_formatter_on_tag = @formatter:on
|
||||||
|
ij_formatter_tags_enabled = true
|
||||||
|
ij_smart_tabs = false
|
||||||
|
ij_visual_guides =
|
||||||
|
ij_wrap_on_typing = false
|
||||||
|
|
||||||
|
[{*.go,*.go2}]
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_go_GROUP_CURRENT_PROJECT_IMPORTS = false
|
||||||
|
ij_go_add_leading_space_to_comments = false
|
||||||
|
ij_go_add_parentheses_for_single_import = false
|
||||||
|
ij_go_call_parameters_new_line_after_left_paren = true
|
||||||
|
ij_go_call_parameters_right_paren_on_new_line = true
|
||||||
|
ij_go_call_parameters_wrap = off
|
||||||
|
ij_go_fill_paragraph_width = 80
|
||||||
|
ij_go_group_stdlib_imports = false
|
||||||
|
ij_go_import_sorting = gofmt
|
||||||
|
ij_go_keep_indents_on_empty_lines = false
|
||||||
|
ij_go_local_group_mode = project
|
||||||
|
ij_go_local_package_prefixes =
|
||||||
|
ij_go_move_all_imports_in_one_declaration = false
|
||||||
|
ij_go_move_all_stdlib_imports_in_one_group = false
|
||||||
|
ij_go_remove_redundant_import_aliases = false
|
||||||
|
ij_go_run_go_fmt_on_reformat = true
|
||||||
|
ij_go_use_back_quotes_for_imports = false
|
||||||
|
ij_go_wrap_comp_lit = off
|
||||||
|
ij_go_wrap_comp_lit_newline_after_lbrace = true
|
||||||
|
ij_go_wrap_comp_lit_newline_before_rbrace = true
|
||||||
|
ij_go_wrap_func_params = off
|
||||||
|
ij_go_wrap_func_params_newline_after_lparen = true
|
||||||
|
ij_go_wrap_func_params_newline_before_rparen = true
|
||||||
|
ij_go_wrap_func_result = off
|
||||||
|
ij_go_wrap_func_result_newline_after_lparen = true
|
||||||
|
ij_go_wrap_func_result_newline_before_rparen = true
|
||||||
|
|
||||||
|
[{*.yaml,*.yml}]
|
||||||
|
indent_size = 2
|
||||||
|
ij_yaml_align_values_properties = do_not_align
|
||||||
|
ij_yaml_autoinsert_sequence_marker = true
|
||||||
|
ij_yaml_block_mapping_on_new_line = false
|
||||||
|
ij_yaml_indent_sequence_value = true
|
||||||
|
ij_yaml_keep_indents_on_empty_lines = false
|
||||||
|
ij_yaml_keep_line_breaks = true
|
||||||
|
ij_yaml_sequence_on_new_line = false
|
||||||
|
ij_yaml_space_before_colon = false
|
||||||
|
ij_yaml_spaces_within_braces = true
|
||||||
|
ij_yaml_spaces_within_brackets = true
|
||||||
|
|
||||||
|
[{*.markdown,*.md}]
|
||||||
|
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||||
|
ij_markdown_force_one_space_after_header_symbol = true
|
||||||
|
ij_markdown_force_one_space_after_list_bullet = true
|
||||||
|
ij_markdown_force_one_space_between_words = true
|
||||||
|
ij_markdown_format_tables = true
|
||||||
|
ij_markdown_insert_quote_arrows_on_wrap = true
|
||||||
|
ij_markdown_keep_indents_on_empty_lines = false
|
||||||
|
ij_markdown_keep_line_breaks_inside_text_blocks = true
|
||||||
|
ij_markdown_max_lines_around_block_elements = 1
|
||||||
|
ij_markdown_max_lines_around_header = 1
|
||||||
|
ij_markdown_max_lines_between_paragraphs = 1
|
||||||
|
ij_markdown_min_lines_around_block_elements = 1
|
||||||
|
ij_markdown_min_lines_around_header = 1
|
||||||
|
ij_markdown_min_lines_between_paragraphs = 1
|
||||||
|
ij_markdown_wrap_text_if_long = true
|
||||||
|
ij_markdown_wrap_text_inside_blockquotes = true
|
||||||
|
|
||||||
|
[{*.json,*.json5,*.jsonc}]
|
||||||
|
indent_size = 2
|
||||||
|
ij_json_array_wrapping = split_into_lines
|
||||||
|
ij_json_keep_blank_lines_in_code = 0
|
||||||
|
ij_json_keep_indents_on_empty_lines = false
|
||||||
|
ij_json_keep_line_breaks = true
|
||||||
|
ij_json_keep_trailing_comma = false
|
||||||
|
ij_json_object_wrapping = split_into_lines
|
||||||
|
ij_json_property_alignment = do_not_align
|
||||||
|
ij_json_space_after_colon = true
|
||||||
|
ij_json_space_after_comma = true
|
||||||
|
ij_json_space_before_colon = false
|
||||||
|
ij_json_space_before_comma = false
|
||||||
|
ij_json_spaces_within_braces = false
|
||||||
|
ij_json_spaces_within_brackets = false
|
||||||
|
ij_json_wrap_long_lines = false
|
||||||
|
|
||||||
|
[.editorconfig]
|
||||||
|
ij_editorconfig_align_group_field_declarations = false
|
||||||
|
ij_editorconfig_space_after_colon = false
|
||||||
|
ij_editorconfig_space_after_comma = true
|
||||||
|
ij_editorconfig_space_before_colon = false
|
||||||
|
ij_editorconfig_space_before_comma = false
|
||||||
|
ij_editorconfig_spaces_around_assignment_operators = true
|
44
.forgejo/workflows/build.yaml
Normal file
44
.forgejo/workflows/build.yaml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [ opened, synchronize, reopened ]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
env:
|
||||||
|
IMAGE_TAG: varakh/upda
|
||||||
|
REVISION: ${{ github.sha }}
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- name: Prepare requirements
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y curl wget bash apt-transport-https ca-certificates gnupg zstd clang
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||||
|
- name: Set up node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '^1.21'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Test native build
|
||||||
|
run: |
|
||||||
|
make ci
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Test docker image build
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_TAG }}:${{ github.sha }}
|
77
.forgejo/workflows/release.yaml
Normal file
77
.forgejo/workflows/release.yaml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
env:
|
||||||
|
VERSION_MAJOR: 1
|
||||||
|
VERSION_MINOR: 0
|
||||||
|
VERSION_PATCH: 0
|
||||||
|
IMAGE_TAG: varakh/upda
|
||||||
|
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda
|
||||||
|
FORGEJO_URL: https://git.myservermanager.com
|
||||||
|
FORGEJO_FQDN: git.myservermanager.com
|
||||||
|
FORGEJO_REPO: varakh/upda
|
||||||
|
REVISION: ${{ github.sha }}
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- name: Prepare requirements
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y curl wget bash apt-transport-https ca-certificates gnupg zstd clang
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||||
|
- name: Set up node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '^1.21'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Test native build
|
||||||
|
run: |
|
||||||
|
make ci
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to Forgejo
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.FORGEJO_FQDN }}
|
||||||
|
username: ${{ secrets.FORGEJO_USER }}
|
||||||
|
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USER }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_KEY }}
|
||||||
|
- name: Build and push docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_TAG_PRIVATE }}:${{ env.VERSION_MAJOR }}
|
||||||
|
${{ env.IMAGE_TAG_PRIVATE }}:${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}
|
||||||
|
${{ env.IMAGE_TAG_PRIVATE }}:${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}.${{ env.VERSION_PATCH }}
|
||||||
|
${{ env.IMAGE_TAG_PRIVATE }}:latest
|
||||||
|
${{ env.IMAGE_TAG }}:${{ env.VERSION_MAJOR }}
|
||||||
|
${{ env.IMAGE_TAG }}:${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}
|
||||||
|
${{ env.IMAGE_TAG }}:${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}.${{ env.VERSION_PATCH }}
|
||||||
|
${{ env.IMAGE_TAG }}:latest
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/forgejo-release@v1
|
||||||
|
with:
|
||||||
|
url: ${{ env.FORGEJO_URL }}
|
||||||
|
repo: ${{ env.FORGEJO_REPO }}
|
||||||
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
direction: upload
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
release-dir: bin
|
||||||
|
release-notes: "Release of version '${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}.${{ env.VERSION_PATCH }}'. For a complete changelog, look into ${{ env.FORGEJO_URL }}/${{ env.FORGEJO_REPO }}/src/tag/${{ github.ref_name }}/CHANGELOG.md"
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
bin/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.run/
|
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
Changes adhere to [semantic versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [1.0.0] - UNRELEASED
|
||||||
|
|
||||||
|
* Initial release
|
49
Dockerfile
Normal file
49
Dockerfile
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#
|
||||||
|
# Build image
|
||||||
|
#
|
||||||
|
FROM alpine:3.18 AS builder
|
||||||
|
LABEL maintainer="Varakh <varakh@varakh.de>"
|
||||||
|
|
||||||
|
RUN apk --update upgrade && \
|
||||||
|
apk add go gcc make sqlite && \
|
||||||
|
# See https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker
|
||||||
|
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN rm -rf bin/ && \
|
||||||
|
CC=gcc make ci
|
||||||
|
|
||||||
|
#
|
||||||
|
# Actual image
|
||||||
|
#
|
||||||
|
FROM alpine:3.18
|
||||||
|
LABEL maintainer="Varakh <varakh@varakh.de>" \
|
||||||
|
description="upda" \
|
||||||
|
org.opencontainers.image.authors="Varakh" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.vendor="Varakh" \
|
||||||
|
org.opencontainers.image.title="upda" \
|
||||||
|
org.opencontainers.image.description="upda" \
|
||||||
|
org.opencontainers.image.base.name="alpine:3.18"
|
||||||
|
|
||||||
|
ENV USER=appuser \
|
||||||
|
GROUP=appuser \
|
||||||
|
UID=2033 \
|
||||||
|
GID=2033
|
||||||
|
|
||||||
|
RUN apk --update upgrade && \
|
||||||
|
apk add sqlite tzdata && \
|
||||||
|
rm -rf /var/cache/apk/* && \
|
||||||
|
addgroup -S ${GROUP} -g ${GID} && \
|
||||||
|
adduser -S ${USER} -G ${GROUP} -u ${UID}
|
||||||
|
|
||||||
|
COPY --from=builder /app/bin/upda-cli-linux-amd64 /usr/bin/upda-cli
|
||||||
|
COPY --from=builder /app/bin/upda-server-linux-amd64 /usr/bin/upda-server
|
||||||
|
|
||||||
|
USER ${USER}
|
||||||
|
|
||||||
|
ENV SERVER_PORT 8080
|
||||||
|
EXPOSE ${SERVER_PORT}
|
||||||
|
CMD ["/usr/bin/upda-server"]
|
674
LICENSE.txt
Normal file
674
LICENSE.txt
Normal file
|
@ -0,0 +1,674 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
62
Makefile
Normal file
62
Makefile
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
BIN_DIR = $(shell pwd)/bin
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf ${BIN_DIR}
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
GO111MODULE=on go mod download
|
||||||
|
|
||||||
|
ci: clean dependencies test-ci build-server-ci build-cli-ci
|
||||||
|
|
||||||
|
build-server-ci: build-server-linux-amd64
|
||||||
|
|
||||||
|
# server requires CGO_ENABLED=1 for go-sqlite
|
||||||
|
build-server-freebsd-amd64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-freebsd-amd64 cmd/server.go
|
||||||
|
build-server-freebsd-arm64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-freebsd-arm64 cmd/server.go
|
||||||
|
build-server-darwin-amd64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-darwin-amd64 cmd/server.go
|
||||||
|
build-server-darwin-arm64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-darwin-arm64 cmd/server.go
|
||||||
|
build-server-linux-amd64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-linux-amd64 cmd/server.go
|
||||||
|
build-server-linux-arm64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-linux-arm64 cmd/server.go
|
||||||
|
build-server-windows-amd64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-windows-amd64 cmd/server.go
|
||||||
|
build-server-windows-arm64:
|
||||||
|
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-windows-arm64 cmd/server.go
|
||||||
|
|
||||||
|
# cli does not require CGO_ENABLED=1, cross-platform build possible
|
||||||
|
build-cli-ci: build-cli-linux-amd64
|
||||||
|
|
||||||
|
build-cli-all: build-cli-freebsd-amd64 build-cli-freebsd-arm64 build-cli-darwin-amd64 build-cli-darwin-arm64 build-cli-linux-amd64 build-cli-linux-arm64 build-cli-windows-amd64 build-cli-windows-arm64
|
||||||
|
|
||||||
|
build-cli-freebsd-amd64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-freebsd-amd64 cmd/cli.go
|
||||||
|
build-cli-freebsd-arm64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-freebsd-arm64 cmd/cli.go
|
||||||
|
build-cli-darwin-amd64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-darwin-amd64 cmd/cli.go
|
||||||
|
build-cli-darwin-arm64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-darwin-arm64 cmd/cli.go
|
||||||
|
build-cli-linux-amd64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-linux-amd64 cmd/cli.go
|
||||||
|
build-cli-linux-arm64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-linux-arm64 cmd/cli.go
|
||||||
|
build-cli-windows-amd64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-windows-amd64 cmd/cli.go
|
||||||
|
build-cli-windows-arm64:
|
||||||
|
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-windows-arm64 cmd/cli.go
|
||||||
|
|
||||||
|
test: test-server test-cli test-util
|
||||||
|
|
||||||
|
test-ci: test
|
||||||
|
|
||||||
|
test-server:
|
||||||
|
GO111MODULE=on go test ./server/...
|
||||||
|
test-cli:
|
||||||
|
GO111MODULE=on go test ./terminal/...
|
||||||
|
test-util:
|
||||||
|
GO111MODULE=on go test ./util/...
|
334
README.md
Normal file
334
README.md
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
# README
|
||||||
|
|
||||||
|
upda - **Up**date **Da**shboard in Go. Please see [motivation](#motivation) and [concepts](#concepts) what this
|
||||||
|
application does.
|
||||||
|
|
||||||
|
There's also a [upda web interface](https://git.myservermanager.com/varakh/upda-ui). It's recommended to take a look (at
|
||||||
|
least at the screenshots).
|
||||||
|
|
||||||
|
In addition, there's a commandline tool called `upda-cli`. For more information, download it and run `./upda-cli help`
|
||||||
|
for further instructions. This is especially useful, if you have an `upda` (server) running and like to invoke webhooks
|
||||||
|
from CLI. `upda-cli` is also bundled in the docker images.
|
||||||
|
|
||||||
|
**See the [deployment instructions](./_doc/DEPLOYMENT.md) for examples on how to deploy upda and upda-ui**
|
||||||
|
|
||||||
|
The main git repository is hosted at
|
||||||
|
_[https://git.myservermanager.com/varakh/upda](https://git.myservermanager.com/varakh/upda)_.
|
||||||
|
Other repositories are mirrors and pull requests, issues, and planning are managed there.
|
||||||
|
|
||||||
|
Contributions are very welcome!
|
||||||
|
|
||||||
|
* [Motivation](#motivation)
|
||||||
|
* [Concepts](#concepts)
|
||||||
|
* [Configuration](#configuration)
|
||||||
|
* [3rd party integrations](#3rd-party-integrations)
|
||||||
|
* [Webhooks](#webhooks)
|
||||||
|
* [Prometheus Metrics](#prometheus-metrics)
|
||||||
|
* [Deployment](#deployment)
|
||||||
|
* [Native](#native)
|
||||||
|
* [Docker](#docker)
|
||||||
|
* [Build docker image](#build-docker-image)
|
||||||
|
* [Development & contribution](#development--contribution)
|
||||||
|
* [Getting started](#getting-started)
|
||||||
|
* [Windows hints](#windows-hints)
|
||||||
|
* [Release](#release)
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
> [duin](https://crazymax.dev/diun/) can determine which OCI images have updates
|
||||||
|
> available. [Argus](https://release-argus.io) can query other sources like GitHub and even invoke actions when an
|
||||||
|
> update
|
||||||
|
> has been found, but there's no _convenient_ way of having **one** dashboard or source of truth for all of them across
|
||||||
|
> different hosts without tinkering with collecting them somewhere in one place. This application is the result of that
|
||||||
|
> tinkering. :-)
|
||||||
|
|
||||||
|
Managing various application or OCI container image updates can be a tedious task:
|
||||||
|
|
||||||
|
* A lot of hosts to operate with a lot of different applications being deployed
|
||||||
|
* A lot of different OCI containers to watch for updated images
|
||||||
|
* No convenient dashboard to see and manage all the available updates in one place
|
||||||
|
|
||||||
|
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to be called
|
||||||
|
via a webhook call (created within _upda_) from other applications, such as a bash script, an
|
||||||
|
application like [duin](https://crazymax.dev/diun/) or simply by using the `upda-cli`.
|
||||||
|
|
||||||
|
After an update is being tracked, _upda_ provides a convenient way to have everything in one place. In addition, it
|
||||||
|
exposes managed _updates_ as [prometheus](https://prometheus.io) metrics, so that you can easily build a dashboard
|
||||||
|
in [Grafana](https://grafana.com), or even attach alerts to pending updates
|
||||||
|
via [alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/).
|
||||||
|
|
||||||
|
In addition, you can use _upda_'s UI to manage updates, e.g. _approve_ them when they have been rolled out to a host.
|
||||||
|
|
||||||
|
Important to note:
|
||||||
|
|
||||||
|
* _upda_ is **NOT a scraper** to watch docker registries or GitHub releases, it simply collects and consolidates updates
|
||||||
|
from different sources via _webhooks_. If you like to watch GitHub releases, write a scraper and use `upda-cli` to
|
||||||
|
report back to _upda_.
|
||||||
|
* _upda_ uses basic auth for administrative tasks like viewing available updates or setting up the initial webhooks.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
_upda_ retrieves new updates when webhooks of upda are invoked, e.g., [duin](https://crazymax.dev/diun/) invokes it
|
||||||
|
or any other application which can reach the instance.
|
||||||
|
Tracked updates are unique for the attributes `(application,provider,host)` which means that subsequent updates for an
|
||||||
|
identical _application_, _provider_ and _host_ simply updates the `version` and `metadata` attributes for that tracked
|
||||||
|
_update_ (regardless if the version or metadata payload _actually_ changed - reasoning behind this is to get reflected
|
||||||
|
metadata updates independent if version attribute has changed).
|
||||||
|
|
||||||
|
State management of tracked updates:
|
||||||
|
|
||||||
|
* On first creation, state is set to _pending_.
|
||||||
|
* When an _update_ is in _approved_ state, an invocation for it resets its state to _pending_.
|
||||||
|
* _Ignored_ updates are skipped entirely and no attribute is updated.
|
||||||
|
|
||||||
|
##### The `application` attribute
|
||||||
|
|
||||||
|
The _application_ attribute is an arbitrary identifier, name or label of a subject you like to track,
|
||||||
|
e.g., `docker.io/varakh/upda` for an OCI image.
|
||||||
|
|
||||||
|
##### The `provider` attribute
|
||||||
|
|
||||||
|
The _provider_ attribute is an arbitrary name or label. During webhook invocation the provider attribute is derived in
|
||||||
|
priority:
|
||||||
|
|
||||||
|
For the _generic_ webhook:
|
||||||
|
|
||||||
|
1. If the incoming payload contains a non-blank `provider` attribute, it's taken from the request.
|
||||||
|
2. If the incoming payload contains a blank or missing `provider` attribute, the issuing webhook's label is taken.
|
||||||
|
|
||||||
|
For the _diun_ webhook:
|
||||||
|
|
||||||
|
1. If the issuing webhook's label is blank, then `oci` is used.
|
||||||
|
2. In any other case, the webhook's label is used.
|
||||||
|
|
||||||
|
Because the first priority is the issuing webhook's label, setting the _same_ label for all webhooks results in a
|
||||||
|
grouping. Also see the _ignore host_ setting for `host` below.
|
||||||
|
|
||||||
|
_Remember that changing a webhook's label won't be reflected in already created/tracked updates!_
|
||||||
|
|
||||||
|
##### The `host` attribute
|
||||||
|
|
||||||
|
_host_ should be set to the originating host name a webhook has been issued from. The _host_
|
||||||
|
attribute can also be "ignored" (a setting in each webhook). If set to ignored, _upda_ sets _host_ to _global_, thus
|
||||||
|
update versions can be grouped independent of the originating host. If set for all webhooks, you'll end up with a host
|
||||||
|
independent update dashboard.
|
||||||
|
|
||||||
|
##### The `version` attribute
|
||||||
|
|
||||||
|
The _version_ attribute is an arbitrary name or label and subject to change across invocations of webhooks. This can be
|
||||||
|
a version number, a number of total updates, anything.
|
||||||
|
|
||||||
|
##### The `metadata` attribute
|
||||||
|
|
||||||
|
An update can hold any additional metadata information provided by request payload `metadata`. Metadata can be inspected
|
||||||
|
via web interface or API.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The following environment variables can be used to modify application behavior.
|
||||||
|
|
||||||
|
| Variable | Purpose | Default/Description |
|
||||||
|
|:-----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
|
||||||
|
| `ADMIN_USER` | Admin user name for login | Not set by default, you need to explicitly set it to user name |
|
||||||
|
| `ADMIN_PASSWORD` | Admin password for login | Not set by default, you need to explicitly set it to a secure random |
|
||||||
|
| | | |
|
||||||
|
| `DB_TYPE` | The database type (Postgres is **recommended**) | Defaults to `sqlite`, possible values are `sqlite` or `postgres` |
|
||||||
|
| `DB_SQLITE_FILE` | Path to the SQLITE file | Defaults to `<XDG_DATA_DIR>/upda/upda.db`, e.g. `~/.local/share/upda/upda.db` |
|
||||||
|
| `DB_POSTGRES_HOST` | The postgres host | Postgres host address, defaults to `localhost` |
|
||||||
|
| `DB_POSTGRES_PORT` | The postgres port | Postgres port, defaults to `5432` |
|
||||||
|
| `DB_POSTGRES_NAME` | The postgres database name | Postgres database name, needs to be set |
|
||||||
|
| `DB_POSTGRES_TZ` | The postgres time zone | Postgres time zone settings, defaults to `Europe/Berlin` |
|
||||||
|
| `DB_POSTGRES_USER` | The postgres user | Postgres user name, needs to be set |
|
||||||
|
| `DB_POSTGRES_PASSWORD` | The postgres password | Postgres user password, needs to be set |
|
||||||
|
| | | |
|
||||||
|
| `SERVER_PORT` | Port | Defaults to `8080` |
|
||||||
|
| `SERVER_LISTEN` | Server's listen address | Defaults to empty which equals `0.0.0.0` |
|
||||||
|
| `SERVER_TLS_ENABLED` | If server uses TLS | Defaults `false` |
|
||||||
|
| `SERVER_TLS_CERT_PATH` | When TLS enabled, provide the certificate path | |
|
||||||
|
| `SERVER_TLS_KEY_PATH` | When TLS enabled, provide the key path | |
|
||||||
|
| `SERVER_TIMEOUT` | Timeout the server waits before shutting down to end any pending tasks | Defaults to `1s` (1 second), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||||
|
| `CORS_ALLOW_ORIGIN` | CORS configuration | Defaults to `*` |
|
||||||
|
| `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
|
||||||
|
| `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` |
|
||||||
|
| | | |
|
||||||
|
| `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal` | Defaults to `info` |
|
||||||
|
| | | |
|
||||||
|
| `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number |
|
||||||
|
| | | |
|
||||||
|
| `TASK_UPDATE_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (ignored/approved) updates from the database | Defaults to `true` |
|
||||||
|
| `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 `true` |
|
||||||
|
| `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 |
|
||||||
|
| `TASK_LOCK_REDIS_ENABLED` | If task locking (multiple instances) is enabled. Requires REDIS. | Defaults to `false` |
|
||||||
|
| `TASK_LOCK_REDIS_URL` | If task locking via REDIS is enabled, this should point to a resolvable REDIS instance, e.g. `redis://<user>:<pass>@localhost:6379/<db>`. | |
|
||||||
|
| | | |
|
||||||
|
| `PROMETHEUS_ENABLED` | If Prometheus metrics are exposed | Defaults to `false` |
|
||||||
|
| `PROMETHEUS_METRICS_PATH` | Defines the metrics endpoint path | Defaults to `/metrics` |
|
||||||
|
| `PROMETHEUS_SECURE_TOKEN_ENABLED` | If Prometheus metrics endpoint is protected by a token when enabled (**recommended**) | Defaults to `true` |
|
||||||
|
| `PROMETHEUS_SECURE_TOKEN` | The token securing the metrics endpoint when enabled (**recommended**) | Not set by default, you need to explicitly set it to a secure random |
|
||||||
|
|
||||||
|
## 3rd party integrations
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
|
||||||
|
This is the core mechanism of _upda_ and why it exists. Webhooks are the central piece of how _upda_ gets notified about
|
||||||
|
updates.
|
||||||
|
|
||||||
|
In order to configure a 3rd party application like [duin](https://crazymax.dev/diun/) to send updates to _upda_ with
|
||||||
|
the [duin webhook notification configuration](https://crazymax.dev/diun/notif/webhook/), **create** a new _upda_ webhook
|
||||||
|
token via _upda_'s web interface or via API call. This gives you
|
||||||
|
|
||||||
|
* a unique _upda_ URL to configure in the notification part of [duin](https://crazymax.dev/diun/),
|
||||||
|
e.g., `/api/v1/webhooks/<a unique identifier>`
|
||||||
|
* a corresponding token for the URL which must be sent as `X-Webhook-Token` header when calling _upda_'s URL
|
||||||
|
|
||||||
|
Expected payload is derived from the _type_ of the webhook which has been created in _upda_.
|
||||||
|
|
||||||
|
Example for [duin Webhook notification](https://crazymax.dev/diun/notif/webhook/) `notif`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notif:
|
||||||
|
webhook:
|
||||||
|
endpoint: https://upda.domain.tld/api/v1/webhooks/ee03cd9e-04d0-4c7f-9866-efe219c2501e
|
||||||
|
method: POST
|
||||||
|
headers:
|
||||||
|
content-type: application/json
|
||||||
|
X-Webhook-Token: <the token from webhook creation in upda>
|
||||||
|
timeout: 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prometheus Metrics
|
||||||
|
|
||||||
|
When `PROMETHEUS_ENABLED` is set to `true`, default metrics about memory utilization, but also custom metrics specific
|
||||||
|
to _upda_ are exposed under the `PROMETHEUS_METRICS_PATH` endpoint.
|
||||||
|
|
||||||
|
A Prometheus scrape configuration might look like the following if `PROMETHEUS_SECURE_TOKEN_ENABLED` is set to `true`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'upda'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['upda:8080']
|
||||||
|
bearer_token: 'VALUE_OF_PROMETHEUS_SECURE_TOKEN'
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom exposed metrics are exposed under the `upda_` namespace.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# HELP upda_updates details for all updates, 0=pending, 1=approved, 2=ignored
|
||||||
|
upda_updates{application="codeberg.org/forgejo/forgejo",host="myserver",provider="oci"} 0
|
||||||
|
upda_updates{application="docker.io/library/mysql",host="myserver",provider="oci"} 2
|
||||||
|
upda_updates{application="quay.io/navidys/prometheus-podman-exporter",host="myserver",provider="oci"} 1
|
||||||
|
upda_updates{application="quay.io/navidys/prometheus-podman-exporter",host="myserver2",provider="oci"} 1
|
||||||
|
# HELP upda_updates_all amount of all updates
|
||||||
|
upda_updates_all 4
|
||||||
|
# HELP upda_updates_approved amount of all updates in approved state
|
||||||
|
upda_updates_approved 2
|
||||||
|
# HELP upda_updates_ignored amount of all updates in ignored state
|
||||||
|
upda_updates_ignored 1
|
||||||
|
# HELP upda_updates_pending amount of all updates in pending state
|
||||||
|
upda_updates_pending 1
|
||||||
|
# HELP upda_webhooks amount of all webhooks
|
||||||
|
upda_webhooks 2
|
||||||
|
# HELP upda_events amount of all events
|
||||||
|
upda_events 146
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Native
|
||||||
|
|
||||||
|
Use the released binary for your platform or run `make clean build-server-{your-platform}` and the binary will be placed
|
||||||
|
into the `bin/` folder.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
For examples how to run, look into [deployment instructions](./_doc/DEPLOYMENT.md) which contains examples
|
||||||
|
for `docker-compose` files.
|
||||||
|
|
||||||
|
#### Build docker image
|
||||||
|
|
||||||
|
To build docker images, do the following
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build --rm --no-cache -t upda:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development & contribution
|
||||||
|
|
||||||
|
* Ensure to set `LOGGING_LEVEL=debug` for proper debug logs during development.
|
||||||
|
* Code guidelines
|
||||||
|
* Each entity has its own repository
|
||||||
|
* Each entity is only used in repository and service (otherwise, mapping happens)
|
||||||
|
* Presenter layer is constructed from the entity, e.g., in REST responses and mapped
|
||||||
|
* No entity is directly returned in any REST response
|
||||||
|
* All log calls should be handled by `zap.L()`
|
||||||
|
* Configuration is bootstrapped via separated `struct` types which are given to the service which need them
|
||||||
|
* Error handling
|
||||||
|
* Always throw an error with `NewServiceError`
|
||||||
|
* Always wrap the cause error with `fmt.Errorf`
|
||||||
|
* Forward/bubble up the error directly, when original error is already a `NewServiceError` (most likely internal
|
||||||
|
calls)
|
||||||
|
* Always abort handler chain with `AbortWithError`
|
||||||
|
* Utils can throw any error
|
||||||
|
|
||||||
|
Please look into the `_doc/` folder for [OpenAPI specification](./_doc/api.yaml) and a Postman Collection.
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
1. Run `make clean dependencies` to fetch dependencies
|
||||||
|
2. Start `git.myservermanager.com/varakh/upda/cmd/server` (or `cli`) as Go application and ensure to have _required_
|
||||||
|
environment variables set
|
||||||
|
|
||||||
|
If you like to test with PSQL and/or REDIS for task locking, here are some useful docker commands to have containers
|
||||||
|
up and running quickly. Set necessary environment variables properly.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# postgres
|
||||||
|
docker run --rm --name=upda-db \
|
||||||
|
-p 5432:5432 \
|
||||||
|
--restart=unless-stopped \
|
||||||
|
-e POSTGRES_USER=upda \
|
||||||
|
-e POSTGRES_PASSWORD=upda \
|
||||||
|
-e POSTGRES_DB=upda \
|
||||||
|
postgres:16-alpine
|
||||||
|
|
||||||
|
# redis
|
||||||
|
docker run --rm --name some-redis \
|
||||||
|
-p 6379:6379 \
|
||||||
|
redis redis-server --save 60 1 --loglevel warning
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows hints
|
||||||
|
|
||||||
|
On Windows, you need a valid `gcc`, e.g., https://jmeubank.github.io/tdm-gcc/download/ and add the `\bin` folder to your
|
||||||
|
path.
|
||||||
|
|
||||||
|
For any `go` command you run, ensure that your `PATH` has the `gcc` binary and that you add `CGO_ENABLED=1` as
|
||||||
|
environment.
|
||||||
|
|
||||||
|
### Release
|
||||||
|
|
||||||
|
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
||||||
|
platform, uploads produced artifacts to that release and publishes docker images automatically.
|
||||||
|
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
||||||
|
built successfully:
|
||||||
|
|
||||||
|
* Adapt `constants_app.go` and change `Version` to the correct version number
|
||||||
|
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||||
|
in footer (link to scm git tag source)
|
||||||
|
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
||||||
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml`
|
||||||
|
|
||||||
|
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||||
|
|
||||||
|
* Adapt `constants_app.go` and change `Version` to the _next_ version number
|
||||||
|
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||||
|
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
||||||
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
162
_doc/DEPLOYMENT.md
Normal file
162
_doc/DEPLOYMENT.md
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
# Deployment
|
||||||
|
|
||||||
|
Use one of the provided `docker-compose` examples, edit to your needs. Then issue `docker compose up` command.
|
||||||
|
|
||||||
|
All applications should be up and running.
|
||||||
|
|
||||||
|
As of now, the web interface and the server comes as different container images.
|
||||||
|
|
||||||
|
Default image user is `appuser` (`uid=2033`) and group is `appgroup` (`gid=2033`).
|
||||||
|
|
||||||
|
The following examples are available
|
||||||
|
|
||||||
|
## Postgres
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
external: false
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: br-upda
|
||||||
|
|
||||||
|
services:
|
||||||
|
ui:
|
||||||
|
container_name: upda_ui
|
||||||
|
image: git.myservermanager.com/varakh/upda-ui:latest
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=https://upda.domain.tld/api/v1/
|
||||||
|
- VITE_APP_TITLE=upda
|
||||||
|
- VITE_APP_DESCRIPTION=upda
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8181:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: upda_api
|
||||||
|
image: git.myservermanager.com/varakh/upda:latest
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- DB_POSTGRES_TZ=Europe/Berlin
|
||||||
|
- DB_TYPE=postgres
|
||||||
|
- DB_POSTGRES_HOST=db
|
||||||
|
- DB_POSTGRES_PORT=5432
|
||||||
|
- DB_POSTGRES_NAME=upda
|
||||||
|
- DB_POSTGRES_USER=upda
|
||||||
|
- DB_POSTGRES_PASSWORD=upda
|
||||||
|
- ADMIN_USER=admin
|
||||||
|
- ADMIN_PASSWORD=changeit
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
container_name: upda_db
|
||||||
|
image: postgres:16
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=upda
|
||||||
|
- POSTGRES_PASSWORD=upda
|
||||||
|
- POSTGRES_DB=upda
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- upda-db-vol:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
upda-db-vol:
|
||||||
|
external: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQLite
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
external: false
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.bridge.name: br-upda
|
||||||
|
|
||||||
|
services:
|
||||||
|
ui:
|
||||||
|
container_name: upda_ui
|
||||||
|
image: git.myservermanager.com/varakh/upda-ui:latest
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=https://upda.domain.tld/api/v1/
|
||||||
|
- VITE_APP_TITLE=upda
|
||||||
|
- VITE_APP_DESCRIPTION=upda
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8181:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: upda_api
|
||||||
|
image: git.myservermanager.com/varakh/upda:latest
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Berlin
|
||||||
|
- DB_FILE=/data/upda.db
|
||||||
|
- ADMIN_USER=admin
|
||||||
|
- ADMIN_PASSWORD=changeit
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
volumes:
|
||||||
|
- upda-app-vol:/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
upda-app-vol:
|
||||||
|
external: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse proxy
|
||||||
|
|
||||||
|
You may want to use a proxy in front of them on your host, e.g., nginx. Here's a configuration snippet which should do
|
||||||
|
the work.
|
||||||
|
|
||||||
|
The UI and API is reachable through the same domain, e.g., `https://upda.domain.tld`. In addition, Let's Encrypt is used
|
||||||
|
for transport encryption.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/upda.domain.tld/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/upda.domain.tld/privkey.pem;
|
||||||
|
|
||||||
|
# ui
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:8181;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# api
|
||||||
|
location ~* ^/(api)/ {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
1308
_doc/api.yaml
Normal file
1308
_doc/api.yaml
Normal file
File diff suppressed because it is too large
Load diff
952
_doc/updaserver.postman_collection.json
Normal file
952
_doc/updaserver.postman_collection.json
Normal file
|
@ -0,0 +1,952 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "cf0931c9-d395-45f4-be89-a43c1b85b925",
|
||||||
|
"name": "updaserver",
|
||||||
|
"description": "API specification",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "updates",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "/updates",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"var jsonData = JSON.parse(responseBody);",
|
||||||
|
"",
|
||||||
|
"if (jsonData.data && jsonData.data.content.length > 0) {",
|
||||||
|
" postman.setEnvironmentVariable(\"updateId\", jsonData.data.content[0].id);",
|
||||||
|
"} else {",
|
||||||
|
" postman.setEnvironmentVariable(\"updateId\", 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}}/updates?page=1&pageSize=10&order=desc&orderBy=updated_at&searchTerm=docker.io&searchIn=application",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "page",
|
||||||
|
"value": "1",
|
||||||
|
"description": "the page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pageSize",
|
||||||
|
"value": "10",
|
||||||
|
"description": "the page size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "order",
|
||||||
|
"value": "desc",
|
||||||
|
"description": "the order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "orderBy",
|
||||||
|
"value": "updated_at",
|
||||||
|
"description": "the order by"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "searchTerm",
|
||||||
|
"value": "docker.io",
|
||||||
|
"description": "the search term"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "searchIn",
|
||||||
|
"value": "application",
|
||||||
|
"description": "the search in"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Finds updates"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/updates/: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}}/updates/:id",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"updates",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{updateId}}",
|
||||||
|
"description": "(Required) the id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes an update by ID"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/updates/:id/state",
|
||||||
|
"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 \"state\": \"pending\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/updates/:id/state",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"updates",
|
||||||
|
":id",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{webhookId}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates webhook"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/updates/: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}}/updates/:id",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"updates",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{updateId}}",
|
||||||
|
"description": "(Required) the id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes an update by ID"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
{
|
||||||
|
"name": "/webhooks",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"var jsonData = JSON.parse(responseBody);",
|
||||||
|
"",
|
||||||
|
"if (jsonData.data && jsonData.data.content.length > 0) {",
|
||||||
|
" postman.setEnvironmentVariable(\"webhookId\", jsonData.data.content[0].id);",
|
||||||
|
" postman.setEnvironmentVariable(\"webhookToken\", jsonData.data.content[0].token);",
|
||||||
|
"} else {",
|
||||||
|
" postman.setEnvironmentVariable(\"webhookId\", 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}}/webhooks?page=1&pageSize=10&order=desc&orderBy=created_at",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "page",
|
||||||
|
"value": "1",
|
||||||
|
"description": "the page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pageSize",
|
||||||
|
"value": "10",
|
||||||
|
"description": "the page size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "order",
|
||||||
|
"value": "desc",
|
||||||
|
"description": "the order"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "orderBy",
|
||||||
|
"value": "created_at",
|
||||||
|
"description": "the order by"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Finds webhooks"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks (generic)",
|
||||||
|
"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 test hook\",\n \"type\": \"generic\",\n \"ignoreHost\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/webhooks",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates webhook"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks (diun)",
|
||||||
|
"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 test hook\",\n \"type\": \"diun\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/webhooks",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates webhook"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks/:id/label",
|
||||||
|
"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 \"label\": \"a new label\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/webhooks/:id/label",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks",
|
||||||
|
":id",
|
||||||
|
"label"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{webhookId}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates webhook"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks/:id/ignore-host",
|
||||||
|
"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 \"ignoreHost\": true\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/webhooks/:id/ignore-host",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks",
|
||||||
|
":id",
|
||||||
|
"ignore-host"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{webhookId}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates webhook"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks/: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}}/webhooks/:id",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{webhookId}}",
|
||||||
|
"description": "(Required) the id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a webhook by ID"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/webhooks/:id (Invoke)",
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "apikey",
|
||||||
|
"apikey": [
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"value": "{{webhookToken}}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "X-Webhook-Token",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "in",
|
||||||
|
"value": "header",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"description": "(Required) the token for the webhook id",
|
||||||
|
"key": "X-Webhook-Token",
|
||||||
|
"value": "tempor labore amet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"application\": \"2\",\n \"provider\": \"cli\",\n \"host\": \"myserver\",\n \"version\": \"1.0.1\",\n \"metadata\": {\n \"key1\": \"val1\",\n \"key2\": \"val2\"\n }\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/webhooks/:id",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"webhooks",
|
||||||
|
":id"
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"value": "{{webhookId}}",
|
||||||
|
"description": "(Required) the id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Invokes a webhook by ID"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/info",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/info",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"info"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Find application information"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/health",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Accept",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/health",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"health"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Find application health"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "/login",
|
||||||
|
"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}}/login",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"login"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Probes login"
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "http://localhost:8080/api/v1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "basicAuthUser",
|
||||||
|
"value": "admin",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "basicAuthPass",
|
||||||
|
"value": "changeit",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "webhookId",
|
||||||
|
"value": "no",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "updateId",
|
||||||
|
"value": "no",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
77
api/constants.go
Normal file
77
api/constants.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
// UpdateState state of an update
|
||||||
|
type UpdateState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UpdateStatePending UpdateState = "pending"
|
||||||
|
UpdateStateApproved UpdateState = "approved"
|
||||||
|
UpdateStateIgnored UpdateState = "ignored"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *UpdateState) Scan(value interface{}) error {
|
||||||
|
*e = UpdateState(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UpdateState) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookType type of webhook
|
||||||
|
type WebhookType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebhookTypeGeneric WebhookType = "generic"
|
||||||
|
WebhookTypeDiun WebhookType = "diun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *WebhookType) Scan(value interface{}) error {
|
||||||
|
*e = WebhookType(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WebhookType) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventName name of event
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *EventName) Scan(value interface{}) error {
|
||||||
|
*e = EventName(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EventName) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventState name of event
|
||||||
|
type EventState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventStateCreated EventState = "created"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *EventState) Scan(value interface{}) error {
|
||||||
|
*e = EventState(value.([]byte))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EventState) Value() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
292
api/dto.go
Normal file
292
api/dto.go
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Requests
|
||||||
|
|
||||||
|
type ModifyUpdateStateRequest struct {
|
||||||
|
State string `json:"state" binding:"required,oneof=pending approved ignored"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyWebhookLabelRequest struct {
|
||||||
|
Label string `json:"label" binding:"required,min=1,max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModifyWebhookIgnoreHostRequest struct {
|
||||||
|
IgnoreHost bool `json:"ignoreHost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateWebhookRequest struct {
|
||||||
|
Label string `json:"label" binding:"required,min=1,max=255"`
|
||||||
|
Type string `json:"type" binding:"required,oneof=generic diun"`
|
||||||
|
IgnoreHost bool `json:"ignoreHost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginateUpdateRequest 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 application provider host version created_at updated_at"`
|
||||||
|
SearchTerm string `form:"searchTerm"`
|
||||||
|
SearchIn string `form:"searchIn,default=application" binding:"oneof=application provider host version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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=desc" binding:"oneof=asc desc"`
|
||||||
|
OrderBy string `form:"orderBy,default=updated_at" binding:"oneof=id label type created_at updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookGenericRequest struct {
|
||||||
|
Application string `json:"application" binding:"required,min=1"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Host string `json:"host" binding:"required,min=1"`
|
||||||
|
Version string `json:"version" binding:"required,min=1"`
|
||||||
|
Metadata interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookDiunMetadataRequest struct {
|
||||||
|
Command string `json:"ctn_command"`
|
||||||
|
CreatedAt string `json:"ctn_createdat"`
|
||||||
|
Id string `json:"ctn_id"`
|
||||||
|
Names string `json:"ctn_names"`
|
||||||
|
Size string `json:"ctn_size"`
|
||||||
|
State string `json:"ctn_state"`
|
||||||
|
Status string `json:"ctn_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookDiunRequest struct {
|
||||||
|
DiunVersion string `json:"diun_version" binding:"required,min=1"`
|
||||||
|
Hostname string `json:"hostname" binding:"required,min=1"`
|
||||||
|
Status string `json:"status" binding:"required,min=1"`
|
||||||
|
Provider string `json:"provider" binding:"required,min=1"`
|
||||||
|
Image string `json:"image" binding:"required,min=1"`
|
||||||
|
HubLink string `json:"hub_link"`
|
||||||
|
MimeType string `json:"mime_type" binding:"required,min=1"`
|
||||||
|
Digest string `json:"digest" binding:"required,min=1"`
|
||||||
|
Created string `json:"created" binding:"required,min=1"`
|
||||||
|
Platform string `json:"platform" binding:"required,min=1"`
|
||||||
|
Metadata WebhookDiunMetadataRequest `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventWindowRequest struct {
|
||||||
|
Size int `form:"size,default=10" binding:"numeric,gte=1"`
|
||||||
|
Skip int `form:"skip,default=0" binding:"numeric"`
|
||||||
|
Order string `form:"order,default=desc" binding:"oneof=asc desc"`
|
||||||
|
OrderBy string `form:"orderBy,default=created_at" binding:"oneof=id name created_at updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responses
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataResponse struct {
|
||||||
|
Response
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
DataResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDataResponseWithPayload(payload interface{}) *DataResponse {
|
||||||
|
e := new(DataResponse)
|
||||||
|
e.Data = payload
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrorResponseWithStatusAndMessage(status string, message string) *ErrorResponse {
|
||||||
|
e := new(ErrorResponse)
|
||||||
|
e.Status = status
|
||||||
|
e.Message = message
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Application string `json:"application"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
State string `json:"state"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
Metadata interface{} `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSingleResponse struct {
|
||||||
|
Data UpdateResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateSingleResponse(id uuid.UUID, application string, provider string, host string, version string, state string, createdAt time.Time, updatedAt time.Time, metadata interface{}) *UpdateSingleResponse {
|
||||||
|
e := new(UpdateSingleResponse)
|
||||||
|
e.Data.ID = id
|
||||||
|
e.Data.Application = application
|
||||||
|
e.Data.Provider = provider
|
||||||
|
e.Data.Host = host
|
||||||
|
e.Data.Version = version
|
||||||
|
e.Data.State = state
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
e.Data.Metadata = metadata
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePageResponse struct {
|
||||||
|
Content []*UpdateResponse `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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDataPageResponse struct {
|
||||||
|
Data *UpdatePageResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdatePageResponse(content []*UpdateResponse, page int, pageSize int, orderBy string, order string, totalElements int64, totalPages int64) *UpdatePageResponse {
|
||||||
|
e := new(UpdatePageResponse)
|
||||||
|
e.Content = content
|
||||||
|
e.Page = page
|
||||||
|
e.PageSize = pageSize
|
||||||
|
e.OrderBy = orderBy
|
||||||
|
e.Order = order
|
||||||
|
e.TotalElements = totalElements
|
||||||
|
e.TotalPages = totalPages
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IgnoreHost bool `json:"ignoreHost"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookSingleResponse struct {
|
||||||
|
Data WebhookResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookSingleResponse(id uuid.UUID, label string, t string, ignoreHost bool, token string, createdAt time.Time, updatedAt time.Time) *WebhookSingleResponse {
|
||||||
|
e := new(WebhookSingleResponse)
|
||||||
|
e.Data.ID = id
|
||||||
|
e.Data.Label = label
|
||||||
|
e.Data.Type = t
|
||||||
|
e.Data.IgnoreHost = ignoreHost
|
||||||
|
e.Data.Token = token
|
||||||
|
e.Data.CreatedAt = createdAt
|
||||||
|
e.Data.UpdatedAt = updatedAt
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookPageResponse struct {
|
||||||
|
Content []*WebhookResponse `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 NewWebhookPageResponse(content []*WebhookResponse, page int, pageSize int, orderBy string, order string, totalElements int64, totalPages int64) *WebhookPageResponse {
|
||||||
|
e := new(WebhookPageResponse)
|
||||||
|
e.Content = content
|
||||||
|
e.Page = page
|
||||||
|
e.PageSize = pageSize
|
||||||
|
e.OrderBy = orderBy
|
||||||
|
e.Order = order
|
||||||
|
e.TotalElements = totalElements
|
||||||
|
e.TotalPages = totalPages
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
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 EventWindowResponse struct {
|
||||||
|
Content []*EventResponse `json:"content"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Skip int `json:"skip"`
|
||||||
|
HasNext bool `json:"hasNext"`
|
||||||
|
OrderBy string `json:"orderBy"`
|
||||||
|
Order string `json:"order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventPayloadUpdateCreatedDto struct {
|
||||||
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
|
Application string `json:"application,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventPayloadUpdateUpdatedDto struct {
|
||||||
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
|
Application string `json:"application,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
VersionPrior string `json:"versionPrior,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
StatePrior string `json:"statePrior,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventPayloadUpdateDeletedDto struct {
|
||||||
|
Application string `json:"application,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventWindowResponse(content []*EventResponse, size int, skip int, orderBy string, order string, hasNext bool) *EventWindowResponse {
|
||||||
|
e := new(EventWindowResponse)
|
||||||
|
e.Content = content
|
||||||
|
e.Size = size
|
||||||
|
e.Skip = skip
|
||||||
|
e.HasNext = hasNext
|
||||||
|
e.OrderBy = orderBy
|
||||||
|
e.Order = order
|
||||||
|
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 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 EventPayloadWebhookDeletedDto struct {
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
IgnoreHost bool `json:"ignoreHost"`
|
||||||
|
}
|
7
cmd/cli.go
Normal file
7
cmd/cli.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.myservermanager.com/varakh/upda/terminal"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
terminal.Start()
|
||||||
|
}
|
7
cmd/server.go
Normal file
7
cmd/server.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.myservermanager.com/varakh/upda/server"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
server.Start()
|
||||||
|
}
|
77
go.mod
Normal file
77
go.mod
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
module git.myservermanager.com/varakh/upda
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Depado/ginprom v1.8.0
|
||||||
|
github.com/adrg/xdg v0.4.0
|
||||||
|
github.com/gin-contrib/cors v1.5.0
|
||||||
|
github.com/gin-contrib/zap v0.2.0
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/go-co-op/gocron v1.37.0
|
||||||
|
github.com/go-co-op/gocron-redis-lock v1.3.0
|
||||||
|
github.com/go-playground/validator/v10 v10.16.0
|
||||||
|
github.com/google/uuid v1.5.0
|
||||||
|
github.com/redis/go-redis/v9 v9.3.1
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/urfave/cli/v2 v2.26.0
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
|
gorm.io/driver/postgres v1.5.4
|
||||||
|
gorm.io/driver/sqlite v1.5.4
|
||||||
|
gorm.io/gorm v1.25.5
|
||||||
|
moul.io/zapgorm2 v1.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bytedance/sonic v1.10.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||||
|
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/gabriel-vasile/mimetype v1.4.2 // 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.10.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.10.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
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||||
|
github.com/prometheus/common v0.44.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.11.1 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/arch v0.5.0 // indirect
|
||||||
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
|
golang.org/x/net v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
359
go.sum
Normal file
359
go.sum
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
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/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
|
github.com/Microsoft/hcsshim v0.11.0 h1:7EFNIY4igHEXUdj1zXgAyU3fLc7QfOKHbkldRVTBdiM=
|
||||||
|
github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
|
||||||
|
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||||
|
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||||
|
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
||||||
|
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||||
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/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/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8=
|
||||||
|
github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4=
|
||||||
|
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=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||||
|
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
|
||||||
|
github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
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/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/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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||||
|
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
||||||
|
github.com/go-co-op/gocron-redis-lock v1.2.0 h1:c5aGtxGxqWfln50Fdx9WpwYwtX7bK8i+pw3aIu2feao=
|
||||||
|
github.com/go-co-op/gocron-redis-lock v1.2.0/go.mod h1:En1WRsLSXsWiRul1GNyKiqniGe6UnrcEs9bOzDBya04=
|
||||||
|
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-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=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
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-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
|
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
|
||||||
|
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||||
|
github.com/go-redsync/redsync/v4 v4.10.0 h1:hTeAak4C73mNBQSTq6KCKDFaiIlfC+z5yTTl8fCJuBs=
|
||||||
|
github.com/go-redsync/redsync/v4 v4.10.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/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/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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/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/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=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
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/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-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/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
|
||||||
|
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
|
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||||
|
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
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/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-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0-rc4/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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/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_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/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||||
|
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||||
|
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/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
|
||||||
|
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
|
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/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||||
|
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.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/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
||||||
|
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.25.0 h1:erH6cQjsaJrH+rJDU9qIf89KFdhK0Bft0aEZHlYC3Vs=
|
||||||
|
github.com/testcontainers/testcontainers-go v0.25.0/go.mod h1:4sC9SiJyzD1XFi59q8umTQYWxnkweEc5OjVtTUlJzqQ=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/redis v0.25.0 h1:Oml2QVZtDfLB8gosT7fRdWsWYSM8vihWKVSl7XZZtv0=
|
||||||
|
github.com/testcontainers/testcontainers-go/modules/redis v0.25.0/go.mod h1:xmCaWbWOQoWzhb5isnZw4GGy4aIaDjq8lSCXYFZz1ME=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
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/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||||
|
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/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/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=
|
||||||
|
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/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/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
|
||||||
|
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-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/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=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
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.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||||
|
google.golang.org/grpc v1.57.0/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=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.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/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||||
|
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||||
|
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=
|
||||||
|
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
|
||||||
|
moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
44
renovate.json5
Normal file
44
renovate.json5
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":rebaseStalePrs",
|
||||||
|
":ignoreUnstable",
|
||||||
|
"group:monorepos",
|
||||||
|
"group:recommended"
|
||||||
|
],
|
||||||
|
"prConcurrentLimit": 0,
|
||||||
|
// skip next alpine, see https://github.com/mattn/go-sqlite3/issues/1164
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchPackageNames": [
|
||||||
|
"alpine"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"major"
|
||||||
|
],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePrefixes": [
|
||||||
|
"github.com/go-co-op/gocron"
|
||||||
|
],
|
||||||
|
"groupName": "gocron"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor"
|
||||||
|
],
|
||||||
|
"groupName": "all minor dependencies",
|
||||||
|
"groupSlug": "all-minor-deps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"groupName": "all patch dependencies",
|
||||||
|
"groupSlug": "all-patch-deps"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
18
server/api_handler_auth.go
Normal file
18
server/api_handler_auth.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthHandler() *authHandler {
|
||||||
|
return &authHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *authHandler) login(c *gin.Context) {
|
||||||
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
77
server/api_handler_error.go
Normal file
77
server/api_handler_error.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errAbortWithValidatorPayload(c *gin.Context, err error) {
|
||||||
|
var errs validator.ValidationErrors
|
||||||
|
errors.As(err, &errs)
|
||||||
|
|
||||||
|
errorMap := make(map[string]string)
|
||||||
|
for _, v := range errs {
|
||||||
|
key, txt := validatorErrorToText(&v)
|
||||||
|
errorMap[key] = txt
|
||||||
|
}
|
||||||
|
|
||||||
|
resErr := newServiceError(IllegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
||||||
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func errToHttpStatus(err error) int {
|
||||||
|
var e *serviceError
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &e):
|
||||||
|
if e.Status == IllegalArgument {
|
||||||
|
return http.StatusBadRequest
|
||||||
|
} else if e.Status == Unauthorized {
|
||||||
|
return http.StatusUnauthorized
|
||||||
|
} else if e.Status == Forbidden {
|
||||||
|
return http.StatusForbidden
|
||||||
|
} else if e.Status == NotFound {
|
||||||
|
return http.StatusNotFound
|
||||||
|
} else if e.Status == Conflict {
|
||||||
|
return http.StatusConflict
|
||||||
|
} else if e.Status == General {
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func errCodeToStr(err error) string {
|
||||||
|
var e *serviceError
|
||||||
|
ok := errors.As(err, &e)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return string(e.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(General)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
||||||
|
x := *e
|
||||||
|
|
||||||
|
switch x.Tag() {
|
||||||
|
case "required":
|
||||||
|
return x.Field(), fmt.Sprintf("%s is required", x.Field())
|
||||||
|
case "max":
|
||||||
|
return x.Field(), fmt.Sprintf("%s cannot be longer than %s", x.Field(), x.Param())
|
||||||
|
case "min":
|
||||||
|
return x.Field(), fmt.Sprintf("%s must be longer than %s", x.Field(), x.Param())
|
||||||
|
case "len":
|
||||||
|
return x.Field(), fmt.Sprintf("%s must be %s characters long", x.Field(), x.Param())
|
||||||
|
}
|
||||||
|
return x.Field(), fmt.Sprintf("%s is not valid", x.Field())
|
||||||
|
}
|
62
server/api_handler_event.go
Normal file
62
server/api_handler_event.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventHandler struct {
|
||||||
|
service eventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEventHandler(s *eventService) *eventHandler {
|
||||||
|
return &eventHandler{service: *s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *eventHandler) window(c *gin.Context) {
|
||||||
|
var queryParams api.EventWindowRequest
|
||||||
|
var err error
|
||||||
|
if err = c.ShouldBindQuery(&queryParams); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []*Event
|
||||||
|
if events, err = h.service.window(queryParams.Size, queryParams.Skip, queryParams.OrderBy, queryParams.Order); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.EventResponse
|
||||||
|
data = make([]*api.EventResponse, 0)
|
||||||
|
|
||||||
|
for _, e := range events {
|
||||||
|
data = append(data, &api.EventResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Name: e.Name,
|
||||||
|
State: e.State,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
Payload: e.Payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNext bool
|
||||||
|
if hasNext, err = h.service.windowHasNext(queryParams.Size, queryParams.Skip, queryParams.OrderBy, queryParams.Order); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewEventWindowResponse(data, queryParams.Size, queryParams.Skip, queryParams.OrderBy, queryParams.Order, hasNext)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *eventHandler) 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)
|
||||||
|
}
|
20
server/api_handler_health.go
Normal file
20
server/api_handler_health.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type healthHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHealthHandler() *healthHandler {
|
||||||
|
return &healthHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *healthHandler) showHealth(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||||
|
"healthy": true,
|
||||||
|
}})
|
||||||
|
}
|
23
server/api_handler_info.go
Normal file
23
server/api_handler_info.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type infoHandler struct {
|
||||||
|
appConfig appConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInfoHandler(a *appConfig) *infoHandler {
|
||||||
|
return &infoHandler{appConfig: *a}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *infoHandler) showInfo(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||||
|
"name": Name,
|
||||||
|
"version": Version,
|
||||||
|
"timeZone": h.appConfig.timeZone,
|
||||||
|
}})
|
||||||
|
}
|
106
server/api_handler_update.go
Normal file
106
server/api_handler_update.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateHandler struct {
|
||||||
|
service updateService
|
||||||
|
appConfig appConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateHandler(s *updateService, c *appConfig) *updateHandler {
|
||||||
|
return &updateHandler{service: *s, appConfig: *c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *updateHandler) paginate(c *gin.Context) {
|
||||||
|
var queryParams api.PaginateUpdateRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindQuery(&queryParams); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates []*Update
|
||||||
|
var err error
|
||||||
|
|
||||||
|
s, stateQueryContainsAtLeastOne := c.GetQueryArray("state")
|
||||||
|
|
||||||
|
var states []api.UpdateState
|
||||||
|
if stateQueryContainsAtLeastOne {
|
||||||
|
for _, state := range s {
|
||||||
|
states = append(states, api.UpdateState(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updates, err = h.service.paginate(queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, queryParams.SearchTerm, queryParams.SearchIn, states...); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.UpdateResponse
|
||||||
|
data = make([]*api.UpdateResponse, 0)
|
||||||
|
|
||||||
|
for _, e := range updates {
|
||||||
|
data = append(data, &api.UpdateResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Application: e.Application,
|
||||||
|
Provider: e.Provider,
|
||||||
|
Host: e.Host,
|
||||||
|
Version: e.Version,
|
||||||
|
State: e.State,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalElements int64
|
||||||
|
if totalElements, err = h.service.count(queryParams.SearchTerm, queryParams.SearchIn, states...); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages := (totalElements + int64(queryParams.PageSize) - 1) / int64(queryParams.PageSize)
|
||||||
|
c.JSON(http.StatusOK, api.NewDataResponseWithPayload(api.NewUpdatePageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *updateHandler) 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.NewUpdateSingleResponse(e.ID, e.Application, e.Provider, e.Host, e.Version, e.State, e.CreatedAt, e.UpdatedAt, e.Metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *updateHandler) updateState(c *gin.Context) {
|
||||||
|
var e *Update
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyUpdateStateRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateState(c.Param("id"), api.UpdateState(req.State)); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewUpdateSingleResponse(e.ID, e.Application, e.Provider, e.Host, e.Version, e.State, e.CreatedAt, e.UpdatedAt, e.Metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *updateHandler) 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)
|
||||||
|
}
|
120
server/api_handler_webhook.go
Normal file
120
server/api_handler_webhook.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookHandler struct {
|
||||||
|
service webhookService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookHandler(s *webhookService) *webhookHandler {
|
||||||
|
return &webhookHandler{service: *s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookHandler) paginate(c *gin.Context) {
|
||||||
|
var queryParams api.PaginateWebhookRequest
|
||||||
|
var err error
|
||||||
|
if err = c.ShouldBindQuery(&queryParams); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var webhooks []*Webhook
|
||||||
|
if webhooks, err = h.service.paginate(queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []*api.WebhookResponse
|
||||||
|
data = make([]*api.WebhookResponse, 0)
|
||||||
|
|
||||||
|
for _, e := range webhooks {
|
||||||
|
data = append(data, &api.WebhookResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
Label: e.Label,
|
||||||
|
Type: e.Type,
|
||||||
|
IgnoreHost: e.IgnoreHost,
|
||||||
|
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.NewWebhookPageResponse(data, queryParams.Page, queryParams.PageSize, queryParams.OrderBy, queryParams.Order, totalElements, totalPages)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookHandler) create(c *gin.Context) {
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.CreateWebhookRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.create(req.Label, api.WebhookType(req.Type), req.IgnoreHost); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.NewWebhookSingleResponse(e.ID, e.Label, e.Type, e.IgnoreHost, e.Token, e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookHandler) updateLabel(c *gin.Context) {
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyWebhookLabelRequest
|
||||||
|
|
||||||
|
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.NewWebhookSingleResponse(e.ID, e.Label, e.Type, e.IgnoreHost, "", e.CreatedAt, e.UpdatedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookHandler) updateIgnoreHost(c *gin.Context) {
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var req api.ModifyWebhookIgnoreHostRequest
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = h.service.updateIgnoreHost(c.Param("id"), req.IgnoreHost); 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) 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)
|
||||||
|
}
|
62
server/api_handler_webhook_invocation.go
Normal file
62
server/api_handler_webhook_invocation.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookInvocationHandler struct {
|
||||||
|
invocationService webhookInvocationService
|
||||||
|
webhookService webhookService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookInvocationHandler(i *webhookInvocationService, w *webhookService) *webhookInvocationHandler {
|
||||||
|
return &webhookInvocationHandler{invocationService: *i, webhookService: *w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *webhookInvocationHandler) executeWebhookGeneric(c *gin.Context) {
|
||||||
|
tokenHeader := c.GetHeader(HeaderWebhookToken)
|
||||||
|
webhookId := c.Param("id")
|
||||||
|
|
||||||
|
var w *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if w, err = h.webhookService.get(webhookId); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch w.Type {
|
||||||
|
case api.WebhookTypeGeneric.Value():
|
||||||
|
var req api.WebhookGenericRequest
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = h.invocationService.executeGeneric(webhookId, tokenHeader, req); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case api.WebhookTypeDiun.Value():
|
||||||
|
var req api.WebhookDiunRequest
|
||||||
|
if err = c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errAbortWithValidatorPayload(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = h.invocationService.executeDiun(webhookId, tokenHeader, req); err != nil {
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
err = newServiceError(IllegalArgument, errors.New("no default handler for webhook type found"))
|
||||||
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
55
server/api_middleware.go
Normal file
55
server/api_middleware.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func middlewareAppName() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header(HeaderAppName, Name)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareAppVersion() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header(HeaderAppVersion, Version)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareAppContentType() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// middlewareErrorHandler handles global error handling, does not overwrite any given status (see -1)
|
||||||
|
func middlewareErrorHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// call next first, so this is the last in chain
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
if len(c.Errors) > 0 {
|
||||||
|
// status -1 doesn't overwrite existing status code
|
||||||
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
|
c.JSON(-1, api.NewErrorResponseWithStatusAndMessage(errCodeToStr(c.Errors.Last()), c.Errors.Last().Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareAppErrorRecoveryHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(General), fmt.Sprintf("%s", err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
150
server/app.go
Normal file
150
server/app.go
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
ginzap "github.com/gin-contrib/zap"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
// configuration init
|
||||||
|
env := bootstrapEnvironment()
|
||||||
|
|
||||||
|
// secure init
|
||||||
|
util.AssertAvailablePRNG()
|
||||||
|
|
||||||
|
// set gin mode derived from logging level
|
||||||
|
if zap.L().Level() == zap.DebugLevel {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(ginzap.Ginzap(zap.L(), time.RFC3339, false))
|
||||||
|
router.Use(ginzap.RecoveryWithZap(zap.L(), true))
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
prometheusService := newPrometheusService(router, env.prometheusConfig)
|
||||||
|
|
||||||
|
if env.prometheusConfig.enabled {
|
||||||
|
prometheusService.init()
|
||||||
|
router.Use(prometheusService.prometheus.Instrument())
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRepo := newUpdateDbRepo(env.db)
|
||||||
|
webhookRepo := newWebhookDbRepo(env.db)
|
||||||
|
eventRepo := newEventDbRepo(env.db)
|
||||||
|
|
||||||
|
eventService := newEventService(eventRepo)
|
||||||
|
updateService := newUpdateService(updateRepo, eventService)
|
||||||
|
webhookService := newWebhookService(webhookRepo, env.webhookConfig, eventService)
|
||||||
|
webhookInvocationService := newWebhookInvocationService(webhookService, updateService, env.webhookConfig)
|
||||||
|
|
||||||
|
taskService := newTaskService(updateService, eventService, webhookService, prometheusService, env.appConfig, env.taskConfig, env.prometheusConfig)
|
||||||
|
taskService.init()
|
||||||
|
taskService.start()
|
||||||
|
|
||||||
|
updateHandler := newUpdateHandler(updateService, env.appConfig)
|
||||||
|
webhookHandler := newWebhookHandler(webhookService)
|
||||||
|
webhookInvocationHandler := newWebhookInvocationHandler(webhookInvocationService, webhookService)
|
||||||
|
eventHandler := newEventHandler(eventService)
|
||||||
|
infoHandler := newInfoHandler(env.appConfig)
|
||||||
|
healthHandler := newHealthHandler()
|
||||||
|
authHandler := newAuthHandler()
|
||||||
|
|
||||||
|
router.Use(middlewareAppName())
|
||||||
|
router.Use(middlewareAppVersion())
|
||||||
|
router.Use(middlewareAppContentType())
|
||||||
|
router.Use(middlewareErrorHandler())
|
||||||
|
router.Use(middlewareAppErrorRecoveryHandler())
|
||||||
|
|
||||||
|
router.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: env.serverConfig.corsAllowOrigin,
|
||||||
|
AllowMethods: env.serverConfig.corsAllowMethods,
|
||||||
|
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
||||||
|
AllowCredentials: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiPublicGroup := router.Group("/api/v1")
|
||||||
|
apiPublicGroup.GET("/health", healthHandler.showHealth)
|
||||||
|
apiPublicGroup.GET("/info", infoHandler.showInfo)
|
||||||
|
|
||||||
|
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.executeWebhookGeneric)
|
||||||
|
|
||||||
|
apiAuthGroup := router.Group("/api/v1", gin.BasicAuth(gin.Accounts{
|
||||||
|
env.authConfig.adminUser: env.authConfig.adminPassword,
|
||||||
|
}))
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/login", authHandler.login)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/updates", updateHandler.paginate)
|
||||||
|
apiAuthGroup.GET("/updates/:id", updateHandler.get)
|
||||||
|
apiAuthGroup.PATCH("/updates/:id/state", updateHandler.updateState)
|
||||||
|
apiAuthGroup.DELETE("/updates/:id", updateHandler.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/webhooks", webhookHandler.paginate)
|
||||||
|
apiAuthGroup.POST("/webhooks", webhookHandler.create)
|
||||||
|
apiAuthGroup.PATCH("/webhooks/:id/label", webhookHandler.updateLabel)
|
||||||
|
apiAuthGroup.PATCH("/webhooks/:id/ignore-host", webhookHandler.updateIgnoreHost)
|
||||||
|
apiAuthGroup.DELETE("/webhooks/:id", webhookHandler.delete)
|
||||||
|
|
||||||
|
apiAuthGroup.GET("/events", eventHandler.window)
|
||||||
|
apiAuthGroup.DELETE("/events/:id", eventHandler.delete)
|
||||||
|
|
||||||
|
// start server
|
||||||
|
serverAddress := fmt.Sprintf("%s:%d", env.serverConfig.listen, env.serverConfig.port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: serverAddress,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if env.serverConfig.tlsEnabled {
|
||||||
|
err = srv.ListenAndServeTLS(env.serverConfig.tlsCertPath, env.serverConfig.tlsKeyPath)
|
||||||
|
} else {
|
||||||
|
err = srv.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
zap.L().Sugar().Fatalf("Application cannot be started: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// gracefully handle shut down
|
||||||
|
// Wait for interrupt signal to gracefully shut down the server with
|
||||||
|
// a timeout of x seconds.
|
||||||
|
quit := make(chan os.Signal)
|
||||||
|
// kill (no param) default send syscall.SIGTERM
|
||||||
|
// kill -2 is syscall.SIGINT
|
||||||
|
// kill -9 is syscall. SIGKILL but cannot be caught, thus no need to add
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
zap.L().Info("Shutting down...")
|
||||||
|
taskService.stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), env.serverConfig.timeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Shutdown failed, exited directly: %v", err)
|
||||||
|
}
|
||||||
|
// catching ctx.Done() for configured timeout
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
zap.L().Sugar().Infof("Shutdown timeout of '%v' expired, exiting...", env.serverConfig.timeout)
|
||||||
|
|
||||||
|
}
|
||||||
|
zap.L().Info("Exited")
|
||||||
|
}
|
11
server/constants_api.go
Normal file
11
server/constants_api.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
const (
|
||||||
|
HeaderAppName = "X-App-Name"
|
||||||
|
HeaderAppVersion = "X-App-Version"
|
||||||
|
|
||||||
|
HeaderWebhookToken = "X-Webhook-Token"
|
||||||
|
|
||||||
|
headerContentType = "Content-Type"
|
||||||
|
headerContentTypeApplicationJson = "application/json"
|
||||||
|
)
|
6
server/constants_app.go
Normal file
6
server/constants_app.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
const (
|
||||||
|
Name = "upda"
|
||||||
|
Version = "1.0.0"
|
||||||
|
)
|
77
server/constants_env.go
Normal file
77
server/constants_env.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
const (
|
||||||
|
envTZ = "TZ"
|
||||||
|
tzDefault = "Europe/Berlin"
|
||||||
|
|
||||||
|
envAdminUser = "ADMIN_USER"
|
||||||
|
envAdminPassword = "ADMIN_PASSWORD"
|
||||||
|
|
||||||
|
envLoggingLevel = "LOGGING_LEVEL"
|
||||||
|
|
||||||
|
envServerPort = "SERVER_PORT"
|
||||||
|
envServerListen = "SERVER_LISTEN"
|
||||||
|
envServerTlsEnabled = "SERVER_TLS_ENABLED"
|
||||||
|
envServerTlsCertPath = "SERVER_TLS_CERT_PATH"
|
||||||
|
envServerTlsKeyPath = "SERVER_TLS_KEY_PATH"
|
||||||
|
envServerTimeout = "SERVER_TIMEOUT"
|
||||||
|
serverListenDefault = ""
|
||||||
|
serverPortDefault = "8080"
|
||||||
|
serverTlsEnabledDefault = "false"
|
||||||
|
serverTimeoutDefault = "1s"
|
||||||
|
|
||||||
|
envCorsAllowOrigin = "CORS_ALLOW_ORIGIN"
|
||||||
|
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
||||||
|
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
||||||
|
corsAllowOriginDefault = "*"
|
||||||
|
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||||
|
corsAllowHeadersDefault = "Authorization, Content-Type"
|
||||||
|
|
||||||
|
dbTypeSqlite = "sqlite"
|
||||||
|
dbTypePostgres = "postgres"
|
||||||
|
|
||||||
|
envDbType = "DB_TYPE"
|
||||||
|
envDbSqliteFile = "DB_SQLITE_FILE"
|
||||||
|
envDbPostgresHost = "DB_POSTGRES_HOST"
|
||||||
|
envDbPostgresPort = "DB_POSTGRES_PORT"
|
||||||
|
envDbPostgresName = "DB_POSTGRES_NAME"
|
||||||
|
envDbPostgresTimeZone = "DB_POSTGRES_TZ"
|
||||||
|
envDbPostgresUser = "DB_POSTGRES_USER"
|
||||||
|
envDbPostgresPassword = "DB_POSTGRES_PASSWORD"
|
||||||
|
dbTypeSqliteDbNameDefault = "upda.db"
|
||||||
|
dbTypePostgresHostDefault = "localhost"
|
||||||
|
dbTypePostgresPortDefault = "5432"
|
||||||
|
dbTypePostgresTZDefault = "Europe/Berlin"
|
||||||
|
|
||||||
|
envTaskPrometheusRefreshInterval = "TASK_PROMETHEUS_REFRESH_INTERVAL"
|
||||||
|
taskPrometheusRefreshDefault = "60s"
|
||||||
|
|
||||||
|
envWebhooksTokenLength = "WEBHOOKS_TOKEN_LENGTH"
|
||||||
|
webhooksTokenLengthDefault = "16"
|
||||||
|
|
||||||
|
envPrometheusEnabled = "PROMETHEUS_ENABLED"
|
||||||
|
envPrometheusMetricsPath = "PROMETHEUS_METRICS_PATH"
|
||||||
|
envPrometheusSecureTokenEnabled = "PROMETHEUS_SECURE_TOKEN_ENABLED"
|
||||||
|
envPrometheusSecureToken = "PROMETHEUS_SECURE_TOKEN"
|
||||||
|
prometheusEnabledDefault = "false"
|
||||||
|
prometheusMetricsPathDefault = "/metrics"
|
||||||
|
prometheusSecureTokenEnabledDefault = "true"
|
||||||
|
|
||||||
|
envTaskUpdateCleanStaleEnabled = "TASK_UPDATE_CLEAN_STALE_ENABLED"
|
||||||
|
envTaskUpdateCleanStaleInterval = "TASK_UPDATE_CLEAN_STALE_INTERVAL"
|
||||||
|
envTaskUpdateCleanStaleMaxAge = "TASK_UPDATE_CLEAN_STALE_MAX_AGE"
|
||||||
|
taskUpdateCleanStaleEnabledDefault = "true"
|
||||||
|
taskUpdateCleanStaleIntervalDefault = "1h"
|
||||||
|
taskUpdateCleanStaleMaxAgeDefault = "168h"
|
||||||
|
|
||||||
|
envTaskEventCleanStaleEnabled = "TASK_EVENT_CLEAN_STALE_ENABLED"
|
||||||
|
envTaskEventCleanStaleInterval = "TASK_EVENT_CLEAN_STALE_INTERVAL"
|
||||||
|
envTaskEventCleanStaleMaxAge = "TASK_EVENT_CLEAN_STALE_MAX_AGE"
|
||||||
|
taskEventCleanStaleEnabledDefault = "true"
|
||||||
|
taskEventCleanStaleIntervalDefault = "8h"
|
||||||
|
taskEventCleanStaleMaxAgeDefault = "2190h"
|
||||||
|
|
||||||
|
envTaskLockRedisEnabled = "TASK_LOCK_REDIS_ENABLED"
|
||||||
|
envTaskLockRedisUrl = "TASK_LOCK_REDIS_URL"
|
||||||
|
taskLockRedisEnabledDefault = "false"
|
||||||
|
)
|
24
server/constants_prometheus.go
Normal file
24
server/constants_prometheus.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
const (
|
||||||
|
metricUpdatesTotal = "updates_all"
|
||||||
|
metricUpdatesTotalHelp = "amount of all updates"
|
||||||
|
|
||||||
|
metricUpdatesPending = "updates_pending"
|
||||||
|
metricUpdatesPendingHelp = "amount of all updates in pending state"
|
||||||
|
|
||||||
|
metricUpdatesIgnored = "updates_ignored"
|
||||||
|
metricUpdatesIgnoredHelp = "amount of all updates in ignored state"
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
87
server/datatype_json_map.go
Normal file
87
server/datatype_json_map.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONMap defined JSON data type, need to implements driver.Valuer, sql.Scanner interface
|
||||||
|
type JSONMap map[string]interface {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value return json value, implement driver.Valuer interface
|
||||||
|
func (m JSONMap) Value() (driver.Value, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
ba, err := m.MarshalJSON()
|
||||||
|
return string(ba), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan scan value into Jsonb, implements sql.Scanner interface
|
||||||
|
func (m *JSONMap) Scan(val interface{}) error {
|
||||||
|
if val == nil {
|
||||||
|
*m = make(JSONMap)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ba []byte
|
||||||
|
switch v := val.(type) {
|
||||||
|
case []byte:
|
||||||
|
ba = v
|
||||||
|
case string:
|
||||||
|
ba = []byte(v)
|
||||||
|
default:
|
||||||
|
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", val))
|
||||||
|
}
|
||||||
|
t := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(ba, &t)
|
||||||
|
*m = t
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON to output non base64 encoded []byte
|
||||||
|
func (m JSONMap) MarshalJSON() ([]byte, error) {
|
||||||
|
if m == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
t := (map[string]interface{})(m)
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON to deserialize []byte
|
||||||
|
func (m *JSONMap) UnmarshalJSON(b []byte) error {
|
||||||
|
t := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(b, &t)
|
||||||
|
*m = t
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GormDataType gorm common data type
|
||||||
|
func (m JSONMap) GormDataType() string {
|
||||||
|
return "jsonmap"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GormDBDataType gorm db data type
|
||||||
|
func (JSONMap) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
||||||
|
switch db.Dialector.Name() {
|
||||||
|
case "sqlite":
|
||||||
|
return "JSON"
|
||||||
|
case "postgres":
|
||||||
|
return "JSONB"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm JSONMap) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
|
||||||
|
data, _ := jm.MarshalJSON()
|
||||||
|
return gorm.Expr("?", string(data))
|
||||||
|
}
|
56
server/entity.go
Normal file
56
server/entity.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *Update) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
u.ID = uuid.New()
|
||||||
|
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"`
|
||||||
|
Application string `gorm:"uniqueIndex:idx_a_p_h;not null"`
|
||||||
|
Provider string `gorm:"uniqueIndex:idx_a_p_h;not null"`
|
||||||
|
Host string `gorm:"uniqueIndex:idx_a_p_h;not null"`
|
||||||
|
Version string `gorm:"not null"`
|
||||||
|
State string `gorm:"not null"`
|
||||||
|
Metadata JSONMap `gorm:"jsonb"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook entity holding information for webhooks
|
||||||
|
type Webhook struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
Type string `gorm:"not null"`
|
||||||
|
Label string `gorm:"not null"`
|
||||||
|
Token string `gorm:"not null"`
|
||||||
|
IgnoreHost bool `gorm:"default:false;not null"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event entity holding information for events
|
||||||
|
type Event struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;unique;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
State string `gorm:"not null"`
|
||||||
|
Payload JSONMap `gorm:"jsonb"`
|
||||||
|
CreatedAt time.Time `gorm:"time;autoCreateTime;not null"`
|
||||||
|
UpdatedAt time.Time `gorm:"time;autoUpdateTime;not null"`
|
||||||
|
}
|
324
server/environment.go
Normal file
324
server/environment.go
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
|
"moul.io/zapgorm2"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appConfig struct {
|
||||||
|
timeZone string
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfig struct {
|
||||||
|
port int
|
||||||
|
listen string
|
||||||
|
tlsEnabled bool
|
||||||
|
tlsCertPath string
|
||||||
|
tlsKeyPath string
|
||||||
|
timeout time.Duration
|
||||||
|
corsAllowOrigin []string
|
||||||
|
corsAllowMethods []string
|
||||||
|
corsAllowHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type authConfig struct {
|
||||||
|
adminUser string
|
||||||
|
adminPassword string
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskConfig struct {
|
||||||
|
updateCleanStaleEnabled bool
|
||||||
|
updateCleanStaleInterval string
|
||||||
|
updateCleanStaleMaxAge time.Duration
|
||||||
|
eventCleanStaleEnabled bool
|
||||||
|
eventCleanStaleInterval string
|
||||||
|
eventCleanStaleMaxAge time.Duration
|
||||||
|
prometheusRefreshInterval string
|
||||||
|
lockRedisEnabled bool
|
||||||
|
lockRedisUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookConfig struct {
|
||||||
|
tokenLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
type prometheusConfig struct {
|
||||||
|
enabled bool
|
||||||
|
path string
|
||||||
|
secureTokenEnabled bool
|
||||||
|
secureToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Environment struct {
|
||||||
|
appConfig *appConfig
|
||||||
|
authConfig *authConfig
|
||||||
|
serverConfig *serverConfig
|
||||||
|
taskConfig *taskConfig
|
||||||
|
webhookConfig *webhookConfig
|
||||||
|
prometheusConfig *prometheusConfig
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func bootstrapEnvironment() *Environment {
|
||||||
|
// logging (configured independently)
|
||||||
|
var logger *zap.Logger
|
||||||
|
var err error
|
||||||
|
level := zap.NewAtomicLevelAt(zapcore.InfoLevel)
|
||||||
|
|
||||||
|
envLoggingLevel := os.Getenv(envLoggingLevel)
|
||||||
|
if envLoggingLevel != "" {
|
||||||
|
if level, err = zap.ParseAtomicLevel(envLoggingLevel); err != nil {
|
||||||
|
log.Fatalf("Cannot parse logging level: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err = zap.NewDevelopment(zap.IncreaseLevel(level))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Can't initialize logger: %v", err)
|
||||||
|
}
|
||||||
|
// flushes buffer, if any
|
||||||
|
defer logger.Sync()
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(logger)
|
||||||
|
|
||||||
|
// assign defaults from given environment variables and validate
|
||||||
|
bootstrapFromEnvironmentAndValidate()
|
||||||
|
|
||||||
|
// parse environment variables in actual configuration structs
|
||||||
|
// app config
|
||||||
|
appConfig := &appConfig{
|
||||||
|
timeZone: os.Getenv(envTZ),
|
||||||
|
}
|
||||||
|
|
||||||
|
// server config
|
||||||
|
var sc *serverConfig
|
||||||
|
|
||||||
|
var serverPort int
|
||||||
|
if serverPort, err = strconv.Atoi(os.Getenv(envServerPort)); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid server port. Reason: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverTlsEnabled := os.Getenv(envServerTlsEnabled) == "true"
|
||||||
|
|
||||||
|
if serverTlsEnabled {
|
||||||
|
failIfEnvKeyNotPresent(envServerTlsCertPath)
|
||||||
|
failIfEnvKeyNotPresent(envServerTlsKeyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverTimeout time.Duration
|
||||||
|
var errParse error
|
||||||
|
if serverTimeout, errParse = time.ParseDuration(os.Getenv(envServerTimeout)); errParse != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not parse timeout. Reason: %s", errParse.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
sc = &serverConfig{
|
||||||
|
port: serverPort,
|
||||||
|
timeout: serverTimeout,
|
||||||
|
listen: os.Getenv(envServerListen),
|
||||||
|
tlsEnabled: serverTlsEnabled,
|
||||||
|
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
||||||
|
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
||||||
|
corsAllowOrigin: []string{os.Getenv(envCorsAllowOrigin)},
|
||||||
|
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
||||||
|
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig := &authConfig{
|
||||||
|
adminUser: os.Getenv(envAdminUser),
|
||||||
|
adminPassword: os.Getenv(envAdminPassword),
|
||||||
|
}
|
||||||
|
|
||||||
|
// task config
|
||||||
|
var tc *taskConfig
|
||||||
|
|
||||||
|
var updateCleanStaleMaxAge time.Duration
|
||||||
|
if updateCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskUpdateCleanStaleMaxAge)); errParse != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale updates. Reason: %s", errParse.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventCleanStaleMaxAge time.Duration
|
||||||
|
if eventCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskEventCleanStaleMaxAge)); errParse != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale events. Reason: %s", errParse.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tc = &taskConfig{
|
||||||
|
updateCleanStaleEnabled: os.Getenv(envTaskUpdateCleanStaleEnabled) == "true",
|
||||||
|
updateCleanStaleInterval: os.Getenv(envTaskUpdateCleanStaleInterval),
|
||||||
|
updateCleanStaleMaxAge: updateCleanStaleMaxAge,
|
||||||
|
eventCleanStaleEnabled: os.Getenv(envTaskEventCleanStaleEnabled) == "true",
|
||||||
|
eventCleanStaleInterval: os.Getenv(envTaskEventCleanStaleInterval),
|
||||||
|
eventCleanStaleMaxAge: eventCleanStaleMaxAge,
|
||||||
|
prometheusRefreshInterval: os.Getenv(envTaskPrometheusRefreshInterval),
|
||||||
|
lockRedisEnabled: os.Getenv(envTaskLockRedisEnabled) == "true",
|
||||||
|
lockRedisUrl: os.Getenv(envTaskLockRedisUrl),
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookTokenLength := 32
|
||||||
|
if webhookTokenLength, err = strconv.Atoi(os.Getenv(envWebhooksTokenLength)); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Invalid webhook token length. Reason: %v", err)
|
||||||
|
}
|
||||||
|
if webhookTokenLength <= 0 {
|
||||||
|
zap.L().Sugar().Fatalln("Invalid webhook token length. Reason: must be a positive number")
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookConfig := &webhookConfig{
|
||||||
|
tokenLength: webhookTokenLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
prometheusConfig := &prometheusConfig{
|
||||||
|
enabled: os.Getenv(envPrometheusEnabled) == "true",
|
||||||
|
path: os.Getenv(envPrometheusMetricsPath),
|
||||||
|
secureTokenEnabled: os.Getenv(envPrometheusSecureTokenEnabled) == "true",
|
||||||
|
secureToken: os.Getenv(envPrometheusSecureToken),
|
||||||
|
}
|
||||||
|
|
||||||
|
if prometheusConfig.enabled && prometheusConfig.secureTokenEnabled {
|
||||||
|
failIfEnvKeyNotPresent(envPrometheusSecureToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// database setup
|
||||||
|
gormLogger := zapgorm2.New(logger)
|
||||||
|
gormLogger.SetAsDefault()
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
zap.L().Sugar().Infof("Using database type '%s'", os.Getenv(envDbType))
|
||||||
|
|
||||||
|
if os.Getenv(envDbType) == dbTypeSqlite {
|
||||||
|
if os.Getenv(envDbSqliteFile) == "" {
|
||||||
|
var defaultDbFile string
|
||||||
|
if defaultDbFile, err = xdg.DataFile(Name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
||||||
|
}
|
||||||
|
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbFile := os.Getenv(envDbSqliteFile)
|
||||||
|
zap.L().Sugar().Infof("Using database file '%s'", dbFile)
|
||||||
|
|
||||||
|
if db, err = gorm.Open(sqlite.Open(dbFile), &gorm.Config{Logger: gormLogger}); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDb, _ := db.DB()
|
||||||
|
sqlDb.SetMaxOpenConns(1)
|
||||||
|
zap.L().Sugar().Infof("SQLite: restricting max connections to '1'")
|
||||||
|
} else if os.Getenv(envDbType) == dbTypePostgres {
|
||||||
|
host := os.Getenv(envDbPostgresHost)
|
||||||
|
port := os.Getenv(envDbPostgresPort)
|
||||||
|
dbUser := os.Getenv(envDbPostgresUser)
|
||||||
|
dbPass := os.Getenv(envDbPostgresPassword)
|
||||||
|
dbName := os.Getenv(envDbPostgresName)
|
||||||
|
dbTZ := os.Getenv(envDbPostgresTimeZone)
|
||||||
|
|
||||||
|
if host == "" || port == "" || dbUser == "" || dbPass == "" || dbName == "" || dbTZ == "" {
|
||||||
|
zap.L().Sugar().Fatalf("Some configuration for database type '%s' is missing", os.Getenv(envDbType))
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=%v", host, dbUser, dbPass, dbName, port, dbTZ)
|
||||||
|
if db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger}); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not setup database: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zap.L().Sugar().Fatalf("Database type '%s' or '%s' is required", dbTypeSqlite, dbTypePostgres)
|
||||||
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not setup database")
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &Environment{appConfig: appConfig,
|
||||||
|
authConfig: authConfig,
|
||||||
|
serverConfig: sc,
|
||||||
|
taskConfig: tc,
|
||||||
|
webhookConfig: webhookConfig,
|
||||||
|
prometheusConfig: prometheusConfig,
|
||||||
|
db: db}
|
||||||
|
|
||||||
|
if err = env.db.AutoMigrate(&Update{}, &Webhook{}, &Event{}); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not migrate database schema: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("appConfig %+v", env.appConfig)
|
||||||
|
zap.L().Sugar().Infof("serverConfig %+v", env.serverConfig)
|
||||||
|
zap.L().Sugar().Infof("taskConfig %+v", env.taskConfig)
|
||||||
|
zap.L().Sugar().Infof("webhookConfig %+v", env.webhookConfig)
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
func bootstrapFromEnvironmentAndValidate() {
|
||||||
|
// app
|
||||||
|
setEnvKeyDefault(envTZ, tzDefault)
|
||||||
|
|
||||||
|
failIfEnvKeyNotPresent(envAdminUser)
|
||||||
|
failIfEnvKeyNotPresent(envAdminPassword)
|
||||||
|
|
||||||
|
// webhook
|
||||||
|
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
||||||
|
|
||||||
|
// task
|
||||||
|
setEnvKeyDefault(envTaskUpdateCleanStaleEnabled, taskUpdateCleanStaleEnabledDefault)
|
||||||
|
setEnvKeyDefault(envTaskUpdateCleanStaleInterval, taskUpdateCleanStaleIntervalDefault)
|
||||||
|
setEnvKeyDefault(envTaskUpdateCleanStaleMaxAge, taskUpdateCleanStaleMaxAgeDefault)
|
||||||
|
|
||||||
|
setEnvKeyDefault(envTaskEventCleanStaleEnabled, taskEventCleanStaleEnabledDefault)
|
||||||
|
setEnvKeyDefault(envTaskEventCleanStaleInterval, taskEventCleanStaleIntervalDefault)
|
||||||
|
setEnvKeyDefault(envTaskEventCleanStaleMaxAge, taskEventCleanStaleMaxAgeDefault)
|
||||||
|
|
||||||
|
setEnvKeyDefault(envTaskPrometheusRefreshInterval, taskPrometheusRefreshDefault)
|
||||||
|
setEnvKeyDefault(envTaskLockRedisEnabled, taskLockRedisEnabledDefault)
|
||||||
|
|
||||||
|
// prometheus
|
||||||
|
setEnvKeyDefault(envPrometheusEnabled, prometheusEnabledDefault)
|
||||||
|
setEnvKeyDefault(envPrometheusMetricsPath, prometheusMetricsPathDefault)
|
||||||
|
setEnvKeyDefault(envPrometheusSecureTokenEnabled, prometheusSecureTokenEnabledDefault)
|
||||||
|
|
||||||
|
// db
|
||||||
|
setEnvKeyDefault(envDbType, dbTypeSqlite)
|
||||||
|
|
||||||
|
if os.Getenv(envDbType) == dbTypePostgres {
|
||||||
|
setEnvKeyDefault(envDbPostgresHost, dbTypePostgresHostDefault)
|
||||||
|
setEnvKeyDefault(envDbPostgresPort, dbTypePostgresPortDefault)
|
||||||
|
setEnvKeyDefault(envDbPostgresTimeZone, dbTypePostgresTZDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
// server
|
||||||
|
setEnvKeyDefault(envServerPort, serverPortDefault)
|
||||||
|
setEnvKeyDefault(envServerListen, serverListenDefault)
|
||||||
|
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
||||||
|
setEnvKeyDefault(envCorsAllowOrigin, corsAllowOriginDefault)
|
||||||
|
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
||||||
|
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
||||||
|
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
func failIfEnvKeyNotPresent(key string) {
|
||||||
|
if os.Getenv(key) == "" {
|
||||||
|
zap.L().Sugar().Fatalf("Not all required ENV variables given. Please set '%s'", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnvKeyDefault(key string, defaultValue string) {
|
||||||
|
var err error
|
||||||
|
if os.Getenv(key) == "" {
|
||||||
|
if err = os.Setenv(key, defaultValue); err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not set default value for ENV variable '%s'", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Set '%s' to '%s'", key, defaultValue)
|
||||||
|
}
|
||||||
|
}
|
48
server/errors.go
Normal file
48
server/errors.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
errorResourceNotFound = newServiceError(NotFound, errors.New("resource not found"))
|
||||||
|
errorResourceAccessDenied = newServiceError(Forbidden, errors.New("resource access denied"))
|
||||||
|
|
||||||
|
errorDatabaseRowsExpected = newServiceDatabaseError(errors.New("action failed, expected affected rows, but got none"))
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
IllegalArgument ErrorCode = "IllegalArgument"
|
||||||
|
Unauthorized ErrorCode = "Unauthorized"
|
||||||
|
Forbidden ErrorCode = "Forbidden"
|
||||||
|
NotFound ErrorCode = "NotFound"
|
||||||
|
Conflict ErrorCode = "Conflict"
|
||||||
|
General ErrorCode = "General"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newServiceError returns an error that formats as the given text and aligns with builtin error
|
||||||
|
func newServiceError(status ErrorCode, err error) error {
|
||||||
|
return &serviceError{status, fmt.Errorf("service error (%v): %w", status, err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newServiceDatabaseError returns an error that formats as the given text and aligns with builtin error
|
||||||
|
func newServiceDatabaseError(error error) error {
|
||||||
|
return newServiceError(General, fmt.Errorf("database error: %w", error))
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceError struct {
|
||||||
|
Status ErrorCode
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *serviceError) Error() string {
|
||||||
|
return fmt.Sprintf("%v", e.Cause)
|
||||||
|
}
|
180
server/repository_event.go
Normal file
180
server/repository_event.go
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventRepository interface {
|
||||||
|
find(id string) (*Event, error)
|
||||||
|
window(size int, skip int, orderBy string, order string) ([]*Event, error)
|
||||||
|
windowHasNext(size int, skip int, orderBy string, order string) (bool, error)
|
||||||
|
count(state ...api.EventState) (int64, error)
|
||||||
|
create(name api.EventName, state api.EventState, payload interface{}) (*Event, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.EventState) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEventDbRepo(db *gorm.DB) *eventDbRepo {
|
||||||
|
return &eventDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) find(id string) (*Event, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Event
|
||||||
|
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 *eventDbRepo) create(name api.EventName, state api.EventState, payload interface{}) (*Event, error) {
|
||||||
|
if name == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Event
|
||||||
|
unmarshalledPayload := JSONMap{}
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
marshalledMetadata, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = unmarshalledPayload.UnmarshalJSON(marshalledMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e = &Event{
|
||||||
|
Name: name.Value(),
|
||||||
|
State: state.Value(),
|
||||||
|
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 *eventDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&Event{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.EventState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
states := make([]string, 0)
|
||||||
|
for _, i := range state {
|
||||||
|
states = append(states, i.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Where("state IN ?", states).Where("updated_at < ?", time).Delete(&Event{}); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) window(size int, skip int, orderBy string, order string) ([]*Event, error) {
|
||||||
|
var e []*Event
|
||||||
|
|
||||||
|
if orderBy == "" {
|
||||||
|
orderBy = "created_at"
|
||||||
|
}
|
||||||
|
if order == "" {
|
||||||
|
order = "asc"
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := r.db.Order(orderBy + " " + order).Offset(skip).Limit(size).Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) windowHasNext(size int, skip int, orderBy string, order string) (bool, error) {
|
||||||
|
if orderBy == "" {
|
||||||
|
orderBy = "created_at"
|
||||||
|
}
|
||||||
|
if order == "" {
|
||||||
|
order = "asc"
|
||||||
|
}
|
||||||
|
|
||||||
|
var e []*Event
|
||||||
|
|
||||||
|
if res := r.db.Order(orderBy + " " + order).Offset(skip + size).Find(&e); res.Error != nil {
|
||||||
|
return false, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(e) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *eventDbRepo) count(state ...api.EventState) (int64, error) {
|
||||||
|
var c int64
|
||||||
|
|
||||||
|
states := make([]string, 0)
|
||||||
|
if len(state) > 0 {
|
||||||
|
for _, s := range state {
|
||||||
|
states = append(states, s.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := r.db.Model(&Event{}).Scopes(allGetEventCriterion(states)).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func criterionEventState(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 allGetEventCriterion(states []string) func(db *gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Scopes(criterionEventState(states))
|
||||||
|
}
|
||||||
|
}
|
302
server/repository_update.go
Normal file
302
server/repository_update.go
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateRepository interface {
|
||||||
|
paginate(page int, pageSize int, orderBy string, order string, searchTerm string, searchIn string, state ...api.UpdateState) ([]*Update, error)
|
||||||
|
count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error)
|
||||||
|
findAll() ([]*Update, error)
|
||||||
|
find(id string) (*Update, error)
|
||||||
|
findBy(application string, provider string, host string) (*Update, error)
|
||||||
|
create(application string, provider string, host string, version string, metadata interface{}) (*Update, error)
|
||||||
|
update(id string, version string, metadata interface{}) (*Update, error)
|
||||||
|
updateState(id string, state api.UpdateState) (*Update, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.UpdateState) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateDbRepo(db *gorm.DB) *updateDbRepo {
|
||||||
|
return &updateDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateDbRepo) findAll() ([]*Update, error) {
|
||||||
|
var e []*Update
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateDbRepo) find(id string) (*Update, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Update
|
||||||
|
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 *updateDbRepo) findBy(application string, provider string, host string) (*Update, error) {
|
||||||
|
if application == "" || provider == "" || host == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Update
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Find(&e, &Update{Application: application, Provider: provider, Host: host}).Limit(1); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.RowsAffected == 0 || e == nil {
|
||||||
|
return nil, errorResourceNotFound
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateDbRepo) create(application string, provider string, host string, version string, metadata interface{}) (*Update, error) {
|
||||||
|
if application == "" || provider == "" || host == "" || version == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Update
|
||||||
|
unmarshalledMetadata := JSONMap{}
|
||||||
|
|
||||||
|
if metadata != nil {
|
||||||
|
marshalledMetadata, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = unmarshalledMetadata.UnmarshalJSON(marshalledMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e = &Update{
|
||||||
|
Application: application,
|
||||||
|
Provider: provider,
|
||||||
|
Host: host,
|
||||||
|
Version: version,
|
||||||
|
State: api.UpdateStatePending.Value(),
|
||||||
|
Metadata: unmarshalledMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *updateDbRepo) updateState(id string, state api.UpdateState) (*Update, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Update
|
||||||
|
|
||||||
|
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 *updateDbRepo) update(id string, version string, metadata interface{}) (*Update, error) {
|
||||||
|
if id == "" || version == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Update
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarshalledMetadata := JSONMap{}
|
||||||
|
|
||||||
|
if metadata != nil {
|
||||||
|
marshalledMetadata, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = unmarshalledMetadata.UnmarshalJSON(marshalledMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Version = version
|
||||||
|
e.Metadata = unmarshalledMetadata
|
||||||
|
|
||||||
|
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 *updateDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&Update{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ...api.UpdateState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
states := make([]string, 0)
|
||||||
|
for _, i := range state {
|
||||||
|
states = append(states, i.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Where("state IN ?", states).Where("updated_at < ?", time).Delete(&Update{}); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, errorValidationPageGreaterZero
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
return nil, errorValidationPageSizeGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var e []*Update
|
||||||
|
|
||||||
|
if orderBy == "" {
|
||||||
|
orderBy = "updated_at"
|
||||||
|
}
|
||||||
|
if order == "" {
|
||||||
|
order = "desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
states := make([]string, 0)
|
||||||
|
if len(state) > 0 {
|
||||||
|
for _, s := range state {
|
||||||
|
states = append(states, s.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := r.db.Scopes(allGetUpdateCriterion(searchTerm, searchIn, states)).Order(orderBy + " " + order).Offset(offset).Limit(pageSize).Find(&e); res.Error != nil {
|
||||||
|
return nil, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *updateDbRepo) count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error) {
|
||||||
|
var c int64
|
||||||
|
|
||||||
|
states := make([]string, 0)
|
||||||
|
if len(state) > 0 {
|
||||||
|
for _, s := range state {
|
||||||
|
states = append(states, s.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := r.db.Model(&Update{}).Scopes(allGetUpdateCriterion(searchTerm, searchIn, states)).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func criterionUpdateSearch(searchTerm string, searchIn string) func(db *gorm.DB) *gorm.DB {
|
||||||
|
if searchTerm == "" || searchIn == "" {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch searchIn {
|
||||||
|
case "host":
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("host LIKE ?", "%"+searchTerm+"%")
|
||||||
|
}
|
||||||
|
case "provider":
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("provider LIKE ?", "%"+searchTerm+"%")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("application LIKE ?", "%"+searchTerm+"%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func criterionUpdateState(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 allGetUpdateCriterion(searchTerm string, searchIn string, states []string) func(db *gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Scopes(criterionUpdateSearch(searchTerm, searchIn), criterionUpdateState(states))
|
||||||
|
}
|
||||||
|
}
|
166
server/repository_webhook.go
Normal file
166
server/repository_webhook.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookRepository interface {
|
||||||
|
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
||||||
|
count() (int64, error)
|
||||||
|
find(id string) (*Webhook, error)
|
||||||
|
create(label string, t api.WebhookType, token string, ignoreHost bool) (*Webhook, error)
|
||||||
|
updateLabel(id string, label string) (*Webhook, error)
|
||||||
|
updateIgnoreHost(id string, ignoreHost bool) (*Webhook, error)
|
||||||
|
delete(id string) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type webhookDbRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookDbRepo(db *gorm.DB) *webhookDbRepo {
|
||||||
|
return &webhookDbRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *webhookDbRepo) find(id string) (*Webhook, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e Webhook
|
||||||
|
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 *webhookDbRepo) create(label string, t api.WebhookType, token string, ignoreHost bool) (*Webhook, error) {
|
||||||
|
if label == "" || t == "" || token == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &Webhook{
|
||||||
|
Label: label,
|
||||||
|
Type: t.Value(),
|
||||||
|
Token: token,
|
||||||
|
IgnoreHost: ignoreHost,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *webhookDbRepo) updateLabel(id string, label string) (*Webhook, error) {
|
||||||
|
if id == "" || label == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Webhook
|
||||||
|
|
||||||
|
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 *webhookDbRepo) updateIgnoreHost(id string, ignoreHost bool) (*Webhook, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var e *Webhook
|
||||||
|
|
||||||
|
if e, err = r.find(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.IgnoreHost = ignoreHost
|
||||||
|
|
||||||
|
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 *webhookDbRepo) delete(id string) (int64, error) {
|
||||||
|
if id == "" {
|
||||||
|
return 0, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var res *gorm.DB
|
||||||
|
if res = r.db.Delete(&Webhook{}, "id = ?", id); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *webhookDbRepo) paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error) {
|
||||||
|
if page == 0 || pageSize <= 0 {
|
||||||
|
return nil, errorValidationPageGreaterZero
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
return nil, errorValidationPageSizeGreaterZero
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var e []*Webhook
|
||||||
|
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 *webhookDbRepo) count() (int64, error) {
|
||||||
|
var c int64
|
||||||
|
var res *gorm.DB
|
||||||
|
|
||||||
|
if res = r.db.Model(&Webhook{}).Count(&c); res.Error != nil {
|
||||||
|
return 0, newServiceDatabaseError(res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
214
server/service_event.go
Normal file
214
server/service_event.go
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventService struct {
|
||||||
|
repo eventRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEventService(r eventRepository) *eventService {
|
||||||
|
return &eventService{
|
||||||
|
repo: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) createUpdateCreated(e *Update) *Event {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.createWithWarnOnly(api.EventNameUpdateCreated, &api.EventPayloadUpdateCreatedDto{
|
||||||
|
ID: e.ID,
|
||||||
|
Application: e.Application,
|
||||||
|
Provider: e.Provider,
|
||||||
|
Host: e.Host,
|
||||||
|
Version: e.Version,
|
||||||
|
State: e.State,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) createUpdateUpdated(old *Update, new *Update) *Event {
|
||||||
|
if old == nil || new == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventName api.EventName
|
||||||
|
|
||||||
|
if old.State == new.State {
|
||||||
|
eventName = api.EventNameUpdateUpdated
|
||||||
|
} else {
|
||||||
|
switch old.State {
|
||||||
|
case api.UpdateStatePending.Value():
|
||||||
|
eventName = api.EventNameUpdateUpdatedPending
|
||||||
|
break
|
||||||
|
case api.UpdateStateApproved.Value():
|
||||||
|
eventName = api.EventNameUpdateUpdatedApproved
|
||||||
|
break
|
||||||
|
case api.UpdateStateIgnored.Value():
|
||||||
|
eventName = api.EventNameUpdateUpdatedIgnored
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.createWithWarnOnly(eventName, &api.EventPayloadUpdateUpdatedDto{
|
||||||
|
ID: new.ID,
|
||||||
|
Application: new.Application,
|
||||||
|
Provider: new.Provider,
|
||||||
|
Host: new.Host,
|
||||||
|
VersionPrior: old.Version,
|
||||||
|
Version: new.Version,
|
||||||
|
StatePrior: old.State,
|
||||||
|
State: new.State,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) createUpdateDeleted(e *Update) *Event {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.createWithWarnOnly(api.EventNameUpdateDeleted, &api.EventPayloadUpdateDeletedDto{
|
||||||
|
Application: e.Application,
|
||||||
|
Provider: e.Provider,
|
||||||
|
Host: e.Host,
|
||||||
|
Version: e.Version,
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if e, err = s.create(name, payload); err != nil {
|
||||||
|
zap.L().Sugar().Warnf("Could not create event '%s': %v", name, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) create(name api.EventName, payload interface{}) (*Event, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Event
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.repo.create(name, api.EventStateCreated, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Created event '%v'", e.Name)
|
||||||
|
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) get(id string) (*Event, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.find(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) delete(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.get(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.repo.delete(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Deleted event '%v'", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) cleanStale(time time.Time, state ...api.EventState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.deleteByUpdatedAtBeforeAndStates(time, state...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) window(size int, skip int, orderBy string, order string) ([]*Event, error) {
|
||||||
|
return s.repo.window(size, skip, orderBy, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) windowHasNext(size int, skip int, orderBy string, order string) (bool, error) {
|
||||||
|
return s.repo.windowHasNext(size, skip, orderBy, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventService) count(state ...api.EventState) (int64, error) {
|
||||||
|
return s.repo.count(state...)
|
||||||
|
}
|
139
server/service_prometheus.go
Normal file
139
server/service_prometheus.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Depado/ginprom"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prometheusService struct {
|
||||||
|
router *gin.Engine
|
||||||
|
prometheus *ginprom.Prometheus
|
||||||
|
config *prometheusConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService {
|
||||||
|
var p *ginprom.Prometheus
|
||||||
|
|
||||||
|
if c.enabled {
|
||||||
|
if c.secureTokenEnabled {
|
||||||
|
p = ginprom.New(
|
||||||
|
ginprom.Engine(r),
|
||||||
|
ginprom.Namespace(Name),
|
||||||
|
ginprom.Subsystem(""),
|
||||||
|
ginprom.Path(c.path),
|
||||||
|
ginprom.Token(c.secureToken),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
p = ginprom.New(
|
||||||
|
ginprom.Engine(r),
|
||||||
|
ginprom.Namespace(Name),
|
||||||
|
ginprom.Subsystem(""),
|
||||||
|
ginprom.Path(c.path),
|
||||||
|
ginprom.Token(c.secureToken),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &prometheusService{
|
||||||
|
prometheus: p,
|
||||||
|
config: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) init() {
|
||||||
|
if !s.config.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = s.registerGaugeNoLabels(metricUpdatesTotal, metricUpdatesTotalHelp)
|
||||||
|
err = s.registerGaugeNoLabels(metricUpdatesPending, metricUpdatesPendingHelp)
|
||||||
|
err = s.registerGaugeNoLabels(metricUpdatesIgnored, metricUpdatesIgnoredHelp)
|
||||||
|
err = s.registerGaugeNoLabels(metricUpdatesApproved, metricUpdatesApprovedHelp)
|
||||||
|
err = s.registerGauge(metricUpdates, metricUpdatesHelp, []string{"application", "provider", "host"})
|
||||||
|
|
||||||
|
err = s.registerGaugeNoLabels(metricWebhooks, metricWebhooksHelp)
|
||||||
|
err = s.registerGaugeNoLabels(metricEvents, metricEventsHelp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Cannot initialize service. Reason: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) registerGaugeNoLabels(name string, help string) error {
|
||||||
|
return s.registerGauge(name, help, make([]string, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) registerGauge(name string, help string, labels []string) error {
|
||||||
|
if !s.config.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" || help == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
s.prometheus.AddCustomGauge(name, help, labels)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) registerCounterNoLabels(name string, help string) error {
|
||||||
|
return s.registerCounter(name, help, make([]string, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) registerCounter(name string, help string, labels []string) error {
|
||||||
|
if !s.config.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" || help == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
s.prometheus.AddCustomCounter(name, help, labels)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) setGaugeNoLabels(name string, value float64) error {
|
||||||
|
return s.setGauge(name, make([]string, 0), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) setGauge(name string, labelValues []string, value float64) error {
|
||||||
|
if !s.config.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.prometheus.SetGaugeValue(name, labelValues, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) increaseCounterNoLabels(name string) error {
|
||||||
|
return s.increaseCounter(name, make([]string, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *prometheusService) increaseCounter(name string, labelValues []string) error {
|
||||||
|
if !s.config.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.prometheus.IncrementCounterValue(name, labelValues); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
206
server/service_task.go
Normal file
206
server/service_task.go
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"github.com/go-co-op/gocron"
|
||||||
|
redislock "github.com/go-co-op/gocron-redis-lock"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type taskService struct {
|
||||||
|
updateService *updateService
|
||||||
|
eventService *eventService
|
||||||
|
webhookService *webhookService
|
||||||
|
prometheusService *prometheusService
|
||||||
|
appConfig *appConfig
|
||||||
|
taskConfig *taskConfig
|
||||||
|
prometheusConfig *prometheusConfig
|
||||||
|
scheduler *gocron.Scheduler
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTaskService(u *updateService, e *eventService, w *webhookService, p *prometheusService, ac *appConfig, tc *taskConfig, pc *prometheusConfig) *taskService {
|
||||||
|
location, err := time.LoadLocation(ac.timeZone)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not initialize correct timezone for scheduler. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gocron.SetPanicHandler(func(jobName string, value any) {
|
||||||
|
zap.L().Sugar().Errorf("Job '%s' had a panic %v", jobName, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduler := gocron.NewScheduler(location)
|
||||||
|
|
||||||
|
if tc.lockRedisEnabled {
|
||||||
|
var redisOptions *redis.Options
|
||||||
|
redisOptions, err = redis.ParseURL(tc.lockRedisUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Cannot parse REDIS URL '%s' to set up locking. Reason: %s", tc.lockRedisUrl, err.Error())
|
||||||
|
}
|
||||||
|
redisClient := redis.NewClient(redisOptions)
|
||||||
|
locker, err := redislock.NewRedisLocker(redisClient, redislock.WithTries(1))
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Cannot set up REDIS locker. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
scheduler.WithDistributedLocker(locker)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &taskService{
|
||||||
|
updateService: u,
|
||||||
|
eventService: e,
|
||||||
|
webhookService: w,
|
||||||
|
prometheusService: p,
|
||||||
|
appConfig: ac,
|
||||||
|
taskConfig: tc,
|
||||||
|
prometheusConfig: pc,
|
||||||
|
scheduler: scheduler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) init() {
|
||||||
|
s.configureCleanupStaleUpdatesTask()
|
||||||
|
s.configureCleanupStaleEventsTask()
|
||||||
|
s.configurePrometheusRefreshTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) stop() {
|
||||||
|
zap.L().Sugar().Infof("Stopping %d periodic tasks...", len(s.scheduler.Jobs()))
|
||||||
|
s.scheduler.Stop()
|
||||||
|
zap.L().Info("Stopped all periodic tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) start() {
|
||||||
|
s.scheduler.StartAsync()
|
||||||
|
zap.L().Sugar().Infof("Started %d periodic tasks", len(s.scheduler.Jobs()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureCleanupStaleUpdatesTask() {
|
||||||
|
if !s.taskConfig.updateCleanStaleEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.scheduler.Every(s.taskConfig.updateCleanStaleInterval).
|
||||||
|
Do(func() {
|
||||||
|
t := time.Now()
|
||||||
|
t = t.Add(-s.taskConfig.updateCleanStaleMaxAge)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var c int64
|
||||||
|
|
||||||
|
if c, err = s.updateService.cleanStale(t, api.UpdateStateApproved, api.UpdateStateIgnored); err != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not clean up ignored or approved updates older than %s (%s). Reason: %s", s.taskConfig.updateCleanStaleMaxAge, t, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c > 0 {
|
||||||
|
zap.L().Sugar().Infof("Cleaned up '%d' stale updates", c)
|
||||||
|
} else {
|
||||||
|
zap.L().Info("No stale updates found to clean up")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not create task for cleaning stale updates. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configureCleanupStaleEventsTask() {
|
||||||
|
if !s.taskConfig.eventCleanStaleEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.scheduler.Every(s.taskConfig.eventCleanStaleInterval).
|
||||||
|
Do(func() {
|
||||||
|
t := time.Now()
|
||||||
|
t = t.Add(-s.taskConfig.eventCleanStaleMaxAge)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var c int64
|
||||||
|
|
||||||
|
if c, err = s.eventService.cleanStale(t, api.EventStateCreated); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if c > 0 {
|
||||||
|
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
|
||||||
|
} else {
|
||||||
|
zap.L().Info("No stale events found to clean up")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not create task for cleaning stale events. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskService) configurePrometheusRefreshTask() {
|
||||||
|
if !s.prometheusConfig.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.scheduler.Every(s.taskConfig.prometheusRefreshInterval).
|
||||||
|
Do(func() {
|
||||||
|
// updates with labels and collect stats about state
|
||||||
|
updates, updatesError := s.updateService.getAll()
|
||||||
|
|
||||||
|
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesTotal, float64(len(updates))); updatesError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh updates all prometheus metric. Reason: %s", updatesError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var pendingTotal int64
|
||||||
|
var ignoredTotal int64
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesPending, float64(pendingTotal)); updatesError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh updates pending prometheus metric. Reason: %s", updatesError.Error())
|
||||||
|
}
|
||||||
|
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesIgnored, float64(ignoredTotal)); updatesError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh updates ignored prometheus metric. Reason: %s", updatesError.Error())
|
||||||
|
}
|
||||||
|
if updatesError = s.prometheusService.setGaugeNoLabels(metricUpdatesApproved, float64(ackTotal)); updatesError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh updates approved prometheus metric. Reason: %s", updatesError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// webhooks
|
||||||
|
var webhooksTotal int64
|
||||||
|
var webhooksError error
|
||||||
|
webhooksTotal, webhooksError = s.webhookService.count()
|
||||||
|
if webhooksError = s.prometheusService.setGaugeNoLabels(metricWebhooks, float64(webhooksTotal)); webhooksError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh webhooks prometheus metric. Reason: %s", webhooksError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// events
|
||||||
|
var eventsTotal int64
|
||||||
|
var eventsError error
|
||||||
|
eventsTotal, eventsError = s.eventService.count()
|
||||||
|
if eventsError = s.prometheusService.setGaugeNoLabels(metricEvents, float64(eventsTotal)); eventsError != nil {
|
||||||
|
zap.L().Sugar().Errorf("Could not refresh events prometheus metric. Reason: %s", eventsError.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Sugar().Fatalf("Could not create task for refreshing prometheus. Reason: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
137
server/service_update.go
Normal file
137
server/service_update.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateService struct {
|
||||||
|
repo updateRepository
|
||||||
|
eventService *eventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateService(r updateRepository, e *eventService) *updateService {
|
||||||
|
return &updateService{
|
||||||
|
repo: r,
|
||||||
|
eventService: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) get(id string) (*Update, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.find(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) getAll() ([]*Update, error) {
|
||||||
|
return s.repo.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) upsert(application string, provider string, host string, version string, metadata interface{}) (*Update, error) {
|
||||||
|
if application == "" || provider == "" || host == "" || version == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Update
|
||||||
|
var err error
|
||||||
|
|
||||||
|
e, err = s.repo.findBy(application, provider, host)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, errorResourceNotFound) {
|
||||||
|
return nil, err
|
||||||
|
} else if err != nil && errors.Is(err, errorResourceNotFound) {
|
||||||
|
if e, err = s.repo.create(application, provider, host, version, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.eventService.createUpdateCreated(e)
|
||||||
|
zap.L().Sugar().Infof("Created update '%v'", e)
|
||||||
|
} else {
|
||||||
|
old := e
|
||||||
|
skip := e.State == api.UpdateStateIgnored.Value()
|
||||||
|
|
||||||
|
if skip {
|
||||||
|
zap.L().Sugar().Infof("Skipping ignored update '%v'", e.ID)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, err = s.repo.update(e.ID.String(), version, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventService.createUpdateUpdated(old, e)
|
||||||
|
zap.L().Sugar().Infof("Updated update '%v'", e)
|
||||||
|
|
||||||
|
if api.UpdateStateApproved.Value() == e.State {
|
||||||
|
zap.L().Sugar().Infof("Setting update '%v' state to '%v'", e.ID, api.UpdateStatePending)
|
||||||
|
if e, err = s.repo.updateState(e.ID.String(), api.UpdateStatePending); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) updateState(id string, state api.UpdateState) (*Update, error) {
|
||||||
|
if id == "" || state == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Update
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldUpdate := e
|
||||||
|
if e, err = s.repo.updateState(id, state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventService.createUpdateUpdated(oldUpdate, e)
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Modified update '%v'", id)
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) delete(id string) error {
|
||||||
|
if id == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Update
|
||||||
|
var err error
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.repo.delete(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventService.createUpdateDeleted(e)
|
||||||
|
|
||||||
|
zap.L().Sugar().Infof("Deleted update '%v'", id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) cleanStale(time time.Time, state ...api.UpdateState) (int64, error) {
|
||||||
|
if len(state) == 0 {
|
||||||
|
return 0, errorValidationNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.deleteByUpdatedAtBeforeAndStates(time, state...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) paginate(page int, pageSize int, orderBy string, order string, searchTerm string, searchIn string, state ...api.UpdateState) ([]*Update, error) {
|
||||||
|
return s.repo.paginate(page, pageSize, orderBy, order, searchTerm, searchIn, state...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *updateService) count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error) {
|
||||||
|
return s.repo.count(searchTerm, searchIn, state...)
|
||||||
|
}
|
130
server/service_webhook.go
Normal file
130
server/service_webhook.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookService struct {
|
||||||
|
repo WebhookRepository
|
||||||
|
webhookConfig *webhookConfig
|
||||||
|
eventService *eventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookService(r WebhookRepository, c *webhookConfig, e *eventService) *webhookService {
|
||||||
|
return &webhookService{
|
||||||
|
repo: r,
|
||||||
|
webhookConfig: c,
|
||||||
|
eventService: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) get(id string) (*Webhook, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := s.repo.find(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) create(label string, t api.WebhookType, ignoreHost bool) (*Webhook, error) {
|
||||||
|
if label == "" || t == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) updateLabel(id string, label string) (*Webhook, error) {
|
||||||
|
if id == "" || label == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) updateIgnoreHost(id string, ignoreHost bool) (*Webhook, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.get(id); err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
s.eventService.createWebhookDeleted(e)
|
||||||
|
zap.L().Sugar().Infof("Deleted webhook '%v'", id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error) {
|
||||||
|
return s.repo.paginate(page, pageSize, orderBy, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookService) count() (int64, error) {
|
||||||
|
return s.repo.count()
|
||||||
|
}
|
105
server/service_webhook_invocation.go
Normal file
105
server/service_webhook_invocation.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
providerDiun = "oci"
|
||||||
|
hostIgnoreReplacement = "global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookInvocationService struct {
|
||||||
|
updateService *updateService
|
||||||
|
webhookService *webhookService
|
||||||
|
webhookConfig *webhookConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebhookInvocationService(w *webhookService, u *updateService, c *webhookConfig) *webhookInvocationService {
|
||||||
|
return &webhookInvocationService{
|
||||||
|
updateService: u,
|
||||||
|
webhookService: w,
|
||||||
|
webhookConfig: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookInvocationService) executeGeneric(id string, token string, req api.WebhookGenericRequest) error {
|
||||||
|
if id == "" || token == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.webhookService.get(id); err != nil {
|
||||||
|
return errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Token != token {
|
||||||
|
return errorResourceAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
host := req.Host
|
||||||
|
if e.IgnoreHost {
|
||||||
|
host = hostIgnoreReplacement
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider string
|
||||||
|
if req.Provider == "" {
|
||||||
|
provider = e.Label
|
||||||
|
} else {
|
||||||
|
provider = req.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.updateService.upsert(req.Application, provider, host, req.Version, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *webhookInvocationService) executeDiun(id string, token string, req api.WebhookDiunRequest) error {
|
||||||
|
if id == "" || token == "" {
|
||||||
|
return errorValidationNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
var e *Webhook
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if e, err = s.webhookService.get(id); err != nil {
|
||||||
|
return errorResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Token != token {
|
||||||
|
return errorResourceAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
host := req.Hostname
|
||||||
|
if e.IgnoreHost {
|
||||||
|
host = hostIgnoreReplacement
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume the "image" attribute has a : separator at the end
|
||||||
|
ss := strings.Split(req.Image, ":")
|
||||||
|
version := ss[len(ss)-1]
|
||||||
|
app := strings.Join(ss, "")
|
||||||
|
app = strings.ReplaceAll(app, version, "")
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
version = req.Digest
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider string
|
||||||
|
if e.Label == "" {
|
||||||
|
provider = providerDiun
|
||||||
|
} else {
|
||||||
|
provider = e.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = s.updateService.upsert(app, provider, host, version, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
337
terminal/app.go
Normal file
337
terminal/app.go
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
|
"git.myservermanager.com/varakh/upda/server"
|
||||||
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
name = "upda-cli"
|
||||||
|
desc = "a commandline helper for upda"
|
||||||
|
version = server.Version
|
||||||
|
|
||||||
|
envServerUrl = "UPDA_SERVER_URL"
|
||||||
|
envAdminUser = "UPDA_ADMIN_USER"
|
||||||
|
envAdminPassword = "UPDA_ADMIN_PASSWORD"
|
||||||
|
envWebhookId = "UPDA_WEBHOOK_ID"
|
||||||
|
envWebhookToken = "UPDA_WEBHOOK_TOKEN"
|
||||||
|
|
||||||
|
flagServerUrl = "server-url"
|
||||||
|
flagAdminUser = "admin-user"
|
||||||
|
flagAdminPass = "admin-pass"
|
||||||
|
flagWebhookId = "webhook-id"
|
||||||
|
flagWebhookToken = "webhook-token"
|
||||||
|
flagUpdatePageSize = "page-size"
|
||||||
|
|
||||||
|
flagRaw = "raw"
|
||||||
|
|
||||||
|
webhooksUrlPath = "/api/v1/webhooks"
|
||||||
|
updatesUrlPath = "/api/v1/updates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
var raw bool
|
||||||
|
var serverUrl string
|
||||||
|
var adminUser string
|
||||||
|
var adminPassword string
|
||||||
|
var webhookId string
|
||||||
|
var webhookToken string
|
||||||
|
var updatePageSize int64
|
||||||
|
|
||||||
|
rawFlag := &cli.BoolFlag{
|
||||||
|
Name: flagRaw,
|
||||||
|
Usage: "on success raw JSON data from response is returned",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Value: false,
|
||||||
|
Destination: &raw,
|
||||||
|
}
|
||||||
|
|
||||||
|
serverUrlFlag := &cli.StringFlag{
|
||||||
|
Name: flagServerUrl,
|
||||||
|
Usage: "the server url (FQDN without context path)",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
EnvVars: []string{envServerUrl},
|
||||||
|
Destination: &serverUrl,
|
||||||
|
}
|
||||||
|
adminUserFlag := &cli.StringFlag{
|
||||||
|
Name: flagAdminUser,
|
||||||
|
Usage: "admin user",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
EnvVars: []string{envAdminUser},
|
||||||
|
Destination: &adminUser,
|
||||||
|
}
|
||||||
|
adminPasswordFlag := &cli.StringFlag{
|
||||||
|
Name: flagAdminPass,
|
||||||
|
Usage: "admin password",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
EnvVars: []string{envAdminPassword},
|
||||||
|
Destination: &adminPassword,
|
||||||
|
}
|
||||||
|
webhookIdFlag := &cli.StringFlag{
|
||||||
|
Name: flagWebhookId,
|
||||||
|
Usage: "webhook id",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
EnvVars: []string{envWebhookId},
|
||||||
|
Destination: &webhookId,
|
||||||
|
}
|
||||||
|
webhookTokenFlag := &cli.StringFlag{
|
||||||
|
Name: flagWebhookToken,
|
||||||
|
Usage: "webhook token",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
EnvVars: []string{envWebhookToken},
|
||||||
|
Destination: &webhookToken,
|
||||||
|
}
|
||||||
|
updatePageSizeFlag := &cli.Int64Flag{
|
||||||
|
Name: flagUpdatePageSize,
|
||||||
|
Usage: "update show page size",
|
||||||
|
Value: 10000,
|
||||||
|
Required: false,
|
||||||
|
Aliases: []string{"ps"},
|
||||||
|
Destination: &updatePageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: "version",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: "show version",
|
||||||
|
}
|
||||||
|
app := &cli.App{
|
||||||
|
Name: name,
|
||||||
|
Usage: desc,
|
||||||
|
Version: version,
|
||||||
|
EnableBashCompletion: true,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "webhook",
|
||||||
|
Aliases: []string{"w"},
|
||||||
|
Usage: "Options for webhook",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Creates a webhook",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
serverUrlFlag,
|
||||||
|
adminUserFlag,
|
||||||
|
adminPasswordFlag,
|
||||||
|
rawFlag,
|
||||||
|
},
|
||||||
|
ArgsUsage: "<label> [<type (generic|diun, default: generic)>] [<ignore-host (true|false, default: false)>]",
|
||||||
|
Action: webhookCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "send",
|
||||||
|
Usage: "Sends data to a webhook",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
serverUrlFlag,
|
||||||
|
webhookIdFlag,
|
||||||
|
webhookTokenFlag,
|
||||||
|
},
|
||||||
|
ArgsUsage: "<json payload>",
|
||||||
|
Action: webhookSend,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "update",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Usage: "Options for update",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "show",
|
||||||
|
Usage: "Shows updates",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
serverUrlFlag,
|
||||||
|
adminUserFlag,
|
||||||
|
adminPasswordFlag,
|
||||||
|
updatePageSizeFlag,
|
||||||
|
rawFlag,
|
||||||
|
},
|
||||||
|
Action: updateShow,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webhookCreate(cCtx *cli.Context) error {
|
||||||
|
if err := failIfFlagsNotPresent(cCtx, []string{flagServerUrl, flagAdminUser, flagAdminPass}); err != nil {
|
||||||
|
return cli.Exit(err, 1)
|
||||||
|
}
|
||||||
|
if !cCtx.Args().Present() {
|
||||||
|
return cli.Exit(errors.New("args required - try 'webhook create help'"), 1)
|
||||||
|
}
|
||||||
|
// validate label
|
||||||
|
label := cCtx.Args().First()
|
||||||
|
if label == "" || len(label) > 255 {
|
||||||
|
return cli.Exit(errors.New("label cannot be blank or only be 255 characters long"), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate type
|
||||||
|
t := cCtx.Args().Get(1)
|
||||||
|
validTypes := []string{api.WebhookTypeGeneric.Value(), api.WebhookTypeDiun.Value()}
|
||||||
|
if t == "" {
|
||||||
|
t = api.WebhookTypeGeneric.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.FindInSlice(validTypes, t) {
|
||||||
|
return cli.Exit(errors.New(fmt.Sprintf("type must be one of %v", validTypes)), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreHost := cCtx.Args().Get(2) == "true"
|
||||||
|
|
||||||
|
// fully constructed payload
|
||||||
|
payload := api.CreateWebhookRequest{
|
||||||
|
Label: label,
|
||||||
|
Type: t,
|
||||||
|
IgnoreHost: ignoreHost,
|
||||||
|
}
|
||||||
|
|
||||||
|
var successRes api.WebhookSingleResponse
|
||||||
|
var errorRes api.ErrorResponse
|
||||||
|
url := cCtx.String(flagServerUrl) + webhooksUrlPath
|
||||||
|
client := resty.New()
|
||||||
|
client.SetDisableWarn(true)
|
||||||
|
res, err := client.R().
|
||||||
|
SetBasicAuth(cCtx.String(flagAdminUser), cCtx.String(flagAdminPass)).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(&payload).
|
||||||
|
SetResult(&successRes).
|
||||||
|
SetError(&errorRes).
|
||||||
|
Post(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(fmt.Errorf("error during webhook creation: %w", err), 1)
|
||||||
|
}
|
||||||
|
if !res.IsSuccess() {
|
||||||
|
return cli.Exit(fmt.Sprintf("error during webhook creation: (%d) %+v", res.StatusCode(), errorRes), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cCtx.Bool(flagRaw) {
|
||||||
|
fmt.Println(string(res.Body()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ID\t%v\n", successRes.Data.ID)
|
||||||
|
fmt.Printf("URL\t%v\n", fmt.Sprintf("%s/%s", url, successRes.Data.ID))
|
||||||
|
fmt.Printf("Token\t%v\n", successRes.Data.Token)
|
||||||
|
fmt.Printf("Type\t%v\n", successRes.Data.Type)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func webhookSend(cCtx *cli.Context) error {
|
||||||
|
if err := failIfFlagsNotPresent(cCtx, []string{flagServerUrl, flagWebhookId, flagWebhookToken}); err != nil {
|
||||||
|
return cli.Exit(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cCtx.Args().Present() || cCtx.Args().Len() < 1 {
|
||||||
|
return cli.Exit(errors.New("args required - try 'webhook send help'"), 1)
|
||||||
|
}
|
||||||
|
// validate payload is valid json
|
||||||
|
payloadArg := cCtx.Args().First()
|
||||||
|
if payloadArg == "" {
|
||||||
|
return cli.Exit(errors.New("payload cannot be blank"), 1)
|
||||||
|
}
|
||||||
|
if !json.Valid([]byte(payloadArg)) {
|
||||||
|
return cli.Exit(errors.New("payload is not valid JSON"), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorRes api.ErrorResponse
|
||||||
|
url := cCtx.String(flagServerUrl) + webhooksUrlPath + "/" + cCtx.String(flagWebhookId)
|
||||||
|
client := resty.New()
|
||||||
|
client.SetDisableWarn(true)
|
||||||
|
res, err := client.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetHeader(server.HeaderWebhookToken, cCtx.String(flagWebhookToken)).
|
||||||
|
SetBody(payloadArg).
|
||||||
|
SetError(&errorRes).
|
||||||
|
Post(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(fmt.Errorf("error during webhook invocation: %w", err), 1)
|
||||||
|
}
|
||||||
|
if !res.IsSuccess() {
|
||||||
|
return cli.Exit(fmt.Sprintf("error during webhook invocation: (%d) %+v", res.StatusCode(), errorRes), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateShow(cCtx *cli.Context) error {
|
||||||
|
if err := failIfFlagsNotPresent(cCtx, []string{flagServerUrl, flagAdminUser, flagAdminPass}); err != nil {
|
||||||
|
return cli.Exit(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var successRes api.UpdateDataPageResponse
|
||||||
|
var errorRes api.ErrorResponse
|
||||||
|
url := cCtx.String(flagServerUrl) + updatesUrlPath + fmt.Sprintf("?pageSize=%d", cCtx.Int64(flagUpdatePageSize))
|
||||||
|
client := resty.New()
|
||||||
|
client.SetDisableWarn(true)
|
||||||
|
res, err := client.R().
|
||||||
|
SetBasicAuth(cCtx.String(flagAdminUser), cCtx.String(flagAdminPass)).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetResult(&successRes).
|
||||||
|
SetError(&errorRes).
|
||||||
|
Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return cli.Exit(fmt.Errorf("error during showing updates: %w", err), 1)
|
||||||
|
}
|
||||||
|
if !res.IsSuccess() {
|
||||||
|
return cli.Exit(fmt.Sprintf("error during showing updates: (%d) %+v", res.StatusCode(), errorRes), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cCtx.Bool(flagRaw) {
|
||||||
|
fmt.Println(string(res.Body()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 10, 1, 1, ' ', tabwriter.Debug)
|
||||||
|
|
||||||
|
for _, u := range successRes.Data.Content {
|
||||||
|
if _, err = fmt.Fprintf(w, "%v\t %v\t %v\t %v\t %v\n", u.Application, u.Host, u.Provider, u.Version, u.State); err != nil {
|
||||||
|
return cli.Exit(fmt.Sprintf("error during showing updates: %+v", errorRes), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = w.Flush(); err != nil {
|
||||||
|
return cli.Exit(fmt.Sprintf("error during showing updates: %+v", errorRes), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func failIfFlagsNotPresent(cCtx *cli.Context, flagKeys []string) error {
|
||||||
|
if flagKeys == nil {
|
||||||
|
return errors.New("flagKeys cannot be null")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range flagKeys {
|
||||||
|
if cCtx.String(key) == "" {
|
||||||
|
return errors.New(fmt.Sprintf("'%v' is required but blank", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
62
util/secure_string.go
Normal file
62
util/secure_string.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssertAvailablePRNG Assert that a cryptographically secure PRNG is available. Panic otherwise.
|
||||||
|
func AssertAvailablePRNG() {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRandomBytes returns securely generated random bytes.
|
||||||
|
// It will return an error if the system's secure random
|
||||||
|
// number generator fails to function correctly, in which
|
||||||
|
// case the caller should not continue.
|
||||||
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
// Note that err == nil only if we read len(b) bytes.
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSecureRandomString returns a securely generated random string.
|
||||||
|
// It will return an error if the system's secure random
|
||||||
|
// number generator fails to function correctly, in which
|
||||||
|
// case the caller should not continue.
|
||||||
|
func GenerateSecureRandomString(n int) (string, error) {
|
||||||
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
|
||||||
|
ret := make([]byte, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ret[i] = letters[num.Int64()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSecureRandomStringURLSafe returns a URL-safe, base64 encoded
|
||||||
|
// securely generated random string.
|
||||||
|
// It will return an error if the system's secure random
|
||||||
|
// number generator fails to function correctly, in which
|
||||||
|
// case the caller should not continue.
|
||||||
|
func GenerateSecureRandomStringURLSafe(n int) (string, error) {
|
||||||
|
b, err := GenerateRandomBytes(n)
|
||||||
|
return base64.URLEncoding.EncodeToString(b), err
|
||||||
|
}
|
53
util/string.go
Normal file
53
util/string.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindInSlice(slice []string, val string) bool {
|
||||||
|
for _, item := range slice {
|
||||||
|
if item == val {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValuesString(m map[string]string) string {
|
||||||
|
values := make([]string, 0, len(m))
|
||||||
|
for _, v := range m {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
return strings.Join(values, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||||
|
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
|
|
||||||
|
func ToSnakeCase(str string) string {
|
||||||
|
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
|
||||||
|
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
|
||||||
|
return strings.ToLower(snake)
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
const (
|
||||||
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomString(n int) string {
|
||||||
|
if n <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := 0; i < n; {
|
||||||
|
if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) {
|
||||||
|
b[i] = letterBytes[idx]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
37
util/string_test.go
Normal file
37
util/string_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindInSlice(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
a.True(FindInSlice([]string{""}, ""))
|
||||||
|
a.True(FindInSlice([]string{"test", "abc"}, "test"))
|
||||||
|
a.False(FindInSlice([]string{"abc"}, "test"))
|
||||||
|
a.False(FindInSlice([]string{""}, "test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSnakeCase(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
a.Equal("test_this", ToSnakeCase("TestThis"))
|
||||||
|
a.Equal("this", ToSnakeCase("This"))
|
||||||
|
a.Equal("", ToSnakeCase(""))
|
||||||
|
a.NotEqual("test_this", ToSnakeCase("Test This"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomString(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
a.Equal("", RandomString(-1))
|
||||||
|
a.Equal(4, len(RandomString(4)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractValuesFromString(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
a.Equal("", ValuesString(nil))
|
||||||
|
a.Equal("val1", ValuesString(map[string]string{"key1": "val1"}))
|
||||||
|
valuesString := ValuesString(map[string]string{"key1": "val1", "key2": "val2"})
|
||||||
|
a.Contains(valuesString, "val1")
|
||||||
|
a.Contains(valuesString, "val2")
|
||||||
|
}
|
Loading…
Reference in a new issue