Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
3bc2cd20eb | |||
2b3d7f8289 | |||
d0312a5853 | |||
f066511eff | |||
ac5b9da086 | |||
a76d55c78b | |||
3d815758ba | |||
7db195af6d | |||
0f9ae11bbb | |||
67e9ce31b0 | |||
c5c6249fa7 | |||
61506f44f1 | |||
3ece069068 | |||
f37ea4fbbb | |||
20588b44bf | |||
4046695f1c | |||
abc78036d0 | |||
9259299a56 | |||
1fc3818d3c | |||
1d79258670 | |||
17592d4fad | |||
302d0b1ad4 | |||
d958f48717 | |||
9679166d0a | |||
47a48523a9 | |||
04a3ef39fa | |||
165b992629 | |||
95074b2a86 | |||
19c367a5d8 | |||
dce287d6a3 |
|
@ -28,7 +28,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: '^1.22'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Test native build
|
||||
|
|
|
@ -3,7 +3,7 @@ on:
|
|||
tags:
|
||||
- '*'
|
||||
env:
|
||||
VERSION_MAJOR: 2
|
||||
VERSION_MAJOR: 4
|
||||
VERSION_MINOR: 0
|
||||
VERSION_PATCH: 1
|
||||
IMAGE_TAG: varakh/upda
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '^1.21'
|
||||
go-version: '^1.22'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Test native build
|
||||
|
|
44
CHANGELOG.md
|
@ -2,6 +2,44 @@
|
|||
|
||||
Changes adhere to [semantic versioning](https://semver.org).
|
||||
|
||||
## [4.0.1] - UNRELEASED
|
||||
|
||||
* ...
|
||||
|
||||
## [4.0.0] - 2024/10/25
|
||||
|
||||
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||
|
||||
* Embed frontend into Go binary and only ship _one_ OCI image
|
||||
* Switch license to GPLv3
|
||||
|
||||
## [3.0.2] - 2024/06/15
|
||||
|
||||
* Don't enforce JSON content type for GET and DELETE requests
|
||||
* Dependency updates
|
||||
* github.com/go-playground/validator/v10 v10.22.0
|
||||
* gorm.io/driver/postgres v1.5.9
|
||||
* gorm.io/driver/sqlite v1.5.6
|
||||
* Fixed filter for Updates ignoring desired state
|
||||
|
||||
## [3.0.1] - 2024/06/10
|
||||
|
||||
* Fixed finding proper remaining Action invocations by their state
|
||||
|
||||
## [3.0.0] - 2024/06/10
|
||||
|
||||
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||
|
||||
* Added automatic detection of `GOMAXPROCS`
|
||||
* Switched to enforce JSON as `Content-Type` for all incoming requests
|
||||
* Switched to properly respond with JSON on page not found or method not allowed
|
||||
* Renamed `CORS_ALLOW_ORIGIN` to `CORS_ALLOW_ORIGINS`
|
||||
* Added `CORS_ALLOW_CREDENTIALS` which defaults to `true`
|
||||
* Added `CORS_EXPOSE_HEADERS` which defaults to `*`
|
||||
* Overhauled package visibility for server module
|
||||
* Updated dependencies
|
||||
* Updated OCI image base to alpine `3.20` with Go `1.22`
|
||||
|
||||
## [2.0.1] - 2024/05/01
|
||||
|
||||
* Fixed retrieval of encrypted webhook token
|
||||
|
@ -45,6 +83,12 @@ Changes adhere to [semantic versioning](https://semver.org).
|
|||
|
||||
* Initial release
|
||||
|
||||
[3.0.2]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.2
|
||||
|
||||
[3.0.1]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.1
|
||||
|
||||
[3.0.0]: https://git.myservermanager.com/varakh/upda/releases/tag/3.0.0
|
||||
|
||||
[2.0.1]: https://git.myservermanager.com/varakh/upda/releases/tag/2.0.1
|
||||
|
||||
[2.0.0]: https://git.myservermanager.com/varakh/upda/releases/tag/2.0.0
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
#
|
||||
# Build image
|
||||
#
|
||||
FROM alpine:3.18 AS builder
|
||||
FROM alpine:3.20 AS builder
|
||||
LABEL maintainer="Varakh <varakh@varakh.de>"
|
||||
|
||||
RUN apk --update upgrade && \
|
||||
apk add go gcc make sqlite && \
|
||||
apk add nodejs npm && \
|
||||
# See https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker
|
||||
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
@ -18,7 +19,7 @@ RUN rm -rf bin/ && \
|
|||
#
|
||||
# Actual image
|
||||
#
|
||||
FROM alpine:3.18
|
||||
FROM alpine:3.20
|
||||
LABEL maintainer="Varakh <varakh@varakh.de>" \
|
||||
description="upda" \
|
||||
org.opencontainers.image.authors="Varakh" \
|
||||
|
@ -26,7 +27,7 @@ LABEL maintainer="Varakh <varakh@varakh.de>" \
|
|||
org.opencontainers.image.vendor="Varakh" \
|
||||
org.opencontainers.image.title="upda" \
|
||||
org.opencontainers.image.description="upda" \
|
||||
org.opencontainers.image.base.name="alpine:3.18"
|
||||
org.opencontainers.image.base.name="alpine:3.20"
|
||||
|
||||
ENV USER=appuser \
|
||||
GROUP=appuser \
|
||||
|
|
694
LICENSE.txt
|
@ -1,64 +1,674 @@
|
|||
License text copyright © 2023 MariaDB plc, All Rights Reserved. "Business Source License" is a trademark of MariaDB plc.
|
||||
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.
|
||||
|
||||
Licensor:
|
||||
Preamble
|
||||
|
||||
Varakh < varakh [at] varakh [dot] de>
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
Licenses Work:
|
||||
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.
|
||||
|
||||
upda (backend, frontend, cli) and all of its related works.
|
||||
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.
|
||||
|
||||
Additional Use Grant:
|
||||
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.
|
||||
|
||||
- Personal, educational and/or non-profit use which does not generate revenue (which includes reducing costs through use of Licenses Work).
|
||||
- Non-profit organizations do not require a commercial license.
|
||||
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.
|
||||
|
||||
Change Date:
|
||||
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.
|
||||
|
||||
- Change date is four years from release date for version 2.0.0
|
||||
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.
|
||||
|
||||
Change License:
|
||||
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.
|
||||
|
||||
Version 2 or later of the GNU General Public License as published by the Free Software Foundation.
|
||||
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
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production
|
||||
use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
||||
0. Definitions.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific
|
||||
version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the
|
||||
terms of the Change License, and the rights granted in the paragraph above terminate.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License,
|
||||
you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must
|
||||
refrain from using the Licensed Work.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this
|
||||
License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each
|
||||
version of the Licensed Work released by Licensor.
|
||||
"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.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the
|
||||
Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply
|
||||
to your use of that work.
|
||||
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.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License
|
||||
for the current and all other versions of the Licensed Work.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may
|
||||
use a trademark or logo of Licensor as expressly required by this License).TO THE EXTENT PERMITTED BY APPLICABLE LAW,
|
||||
THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR
|
||||
IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
NON-INFRINGEMENT, AND TITLE.
|
||||
MariaDB hereby grants you permission to use this License's text to license your works, and
|
||||
to refer to it using the trademark "Business Source License", as long as you comply with the Covenants of Licensor
|
||||
below.
|
||||
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.
|
||||
|
||||
Notice
|
||||
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.
|
||||
|
||||
The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work
|
||||
will eventually be made available under an Open Source License, as stated in this License.
|
||||
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>.
|
||||
|
|
85
Makefile
|
@ -1,62 +1,87 @@
|
|||
BIN_DIR = $(shell pwd)/bin
|
||||
WEB_DIR = $(shell pwd)/server/web
|
||||
WEB_BUILD_DIR = $(shell pwd)/server/web/build
|
||||
WEB_NODE_DIR = $(shell pwd)/server/web/node_modules
|
||||
WEB_COVERAGE_DIR = $(shell pwd)/server/web/coverage
|
||||
|
||||
clean:
|
||||
# cleanup steps
|
||||
clean: clean-server clean-web
|
||||
clean-server:
|
||||
rm -rf ${BIN_DIR}
|
||||
clean-web:
|
||||
rm -rf ${WEB_BUILD_DIR} ${WEB_NODE_DIR} ${WEB_COVERAGE_DIR}
|
||||
|
||||
dependencies:
|
||||
# dependencies steps
|
||||
dependencies: dependencies-web dependencies-server
|
||||
dependencies-server:
|
||||
GO111MODULE=on go mod download
|
||||
dependencies-web:
|
||||
cd ${WEB_DIR}; npm install
|
||||
|
||||
ci: clean dependencies test-ci build-server-ci build-cli-ci
|
||||
# checkstyle steps
|
||||
checkstyle: checkstyle-web checkstyle-server
|
||||
checkstyle-server:
|
||||
go vet ./...
|
||||
checkstyle-web:
|
||||
cd ${WEB_DIR}; npm run checkstyle
|
||||
|
||||
build-server-ci: build-server-linux-amd64
|
||||
# test steps
|
||||
test: test-web test-server
|
||||
test-server:
|
||||
go test -race ./...
|
||||
test-web:
|
||||
cd ${WEB_DIR}; npm run test:ci
|
||||
|
||||
# build steps
|
||||
|
||||
# server requires CGO_ENABLED=1 for go-sqlite
|
||||
build-server-freebsd-amd64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-freebsd-amd64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-freebsd-amd64 cmd/server/main.go
|
||||
build-server-freebsd-arm64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-freebsd-arm64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-freebsd-arm64 cmd/server/main.go
|
||||
build-server-darwin-amd64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-darwin-amd64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-darwin-amd64 cmd/server/main.go
|
||||
build-server-darwin-arm64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-darwin-arm64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-darwin-arm64 cmd/server/main.go
|
||||
build-server-linux-amd64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-linux-amd64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-linux-amd64 cmd/server/main.go
|
||||
build-server-linux-arm64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-linux-arm64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-linux-arm64 cmd/server/main.go
|
||||
build-server-windows-amd64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-server-windows-amd64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-server-windows-amd64 cmd/server/main.go
|
||||
build-server-windows-arm64:
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-server-windows-arm64 cmd/server.go
|
||||
CGO_ENABLED=1 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-server-windows-arm64 cmd/server/main.go
|
||||
|
||||
# cli does not require CGO_ENABLED=1, cross-platform build possible
|
||||
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
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-freebsd-amd64 cmd/cli/main.go
|
||||
build-cli-freebsd-arm64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-freebsd-arm64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=freebsd GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-freebsd-arm64 cmd/cli/main.go
|
||||
build-cli-darwin-amd64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-darwin-amd64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-darwin-amd64 cmd/cli/main.go
|
||||
build-cli-darwin-arm64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-darwin-arm64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=darwin GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-darwin-arm64 cmd/cli/main.go
|
||||
build-cli-linux-amd64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-linux-amd64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-linux-amd64 cmd/cli/main.go
|
||||
build-cli-linux-arm64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-linux-arm64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=linux GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-linux-arm64 cmd/cli/main.go
|
||||
build-cli-windows-amd64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -o ${BIN_DIR}/upda-cli-windows-amd64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=amd64 go build -tags prod -o ${BIN_DIR}/upda-cli-windows-amd64 cmd/cli/main.go
|
||||
build-cli-windows-arm64:
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -o ${BIN_DIR}/upda-cli-windows-arm64 cmd/cli.go
|
||||
CGO_ENABLED=0 GO111MODULE=on GOOS=windows GOARCH=arm64 go build -tags prod -o ${BIN_DIR}/upda-cli-windows-arm64 cmd/cli/main.go
|
||||
|
||||
test: test-server test-cli test-util
|
||||
# remove built build/conf directory to be served live from the running binary
|
||||
build-web:
|
||||
cd ${WEB_DIR}; npm run build; rm -rf build/conf
|
||||
|
||||
# ci
|
||||
clean-ci: clean
|
||||
dependencies-ci: dependencies
|
||||
checkstyle-ci: checkstyle
|
||||
test-ci: test
|
||||
|
||||
test-server:
|
||||
GO111MODULE=on go test ./server/...
|
||||
test-cli:
|
||||
GO111MODULE=on go test ./terminal/...
|
||||
test-util:
|
||||
GO111MODULE=on go test ./util/...
|
||||
build-server-ci: build-server-linux-amd64
|
||||
build-cli-ci: build-cli-linux-amd64
|
||||
build-web-ci: build-web
|
||||
ci: clean-ci dependencies-ci checkstyle-ci test-ci build-web-ci build-server-ci build-cli-ci
|
||||
|
|
94
README.md
|
@ -1,6 +1,6 @@
|
|||
# README
|
||||
|
||||
Backend for upda - **Up**date **Da**shboard in Go.
|
||||
upda - **Up**date **Da**shboard in Go.
|
||||
|
||||
The main git repository is hosted at
|
||||
_[https://git.myservermanager.com/varakh/upda](https://git.myservermanager.com/varakh/upda)_.
|
||||
|
@ -8,11 +8,48 @@ Other repositories are mirrors and pull requests, issues, and planning are manag
|
|||
|
||||
Contributions are very welcome!
|
||||
|
||||
[Official documentation](https://git.myservermanager.com/varakh/upda-docs) is hosted in a separate git repository.
|
||||
See [official documentation](./_doc/Home.md).
|
||||
|
||||
## Development & contribution
|
||||
|
||||
* Ensure to set the following environment variables for proper debug logs during development
|
||||
There's also a [embedded frontend](#embedded-frontend).
|
||||
|
||||
* Pay attention to `make checkstyle` (uses `go vet ./...`); pipeline fails if issues are detected.
|
||||
* 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` for repositories, services and handlers
|
||||
* Always throw an error wrapping the cause with `fmt.Errorf`
|
||||
* Forward/bubble up the error directly, when original error is already a `NewServiceError` (most likely internal
|
||||
calls)
|
||||
* Always abort handler chain with `AbortWithError`
|
||||
* Utils can throw any error
|
||||
* Repositories, handlers and services should always properly return `error` including any `init`-like function (
|
||||
best to avoid them and call in `newXXX`). **Do not abort with `Fatalf` or similar**
|
||||
* `log.Fatalf` or `zap.L().Fatal` is allowed in `environment.go` or `app.go`
|
||||
* Look into the `_doc/` folder for [OpenAPI specification](./_doc/api.yaml) and a Postman Collection.
|
||||
* Consider reading [Effective Go](https://go.dev/doc/effective_go)
|
||||
* Consider reading [100 Go Mistakes and How to Avoid Them](https://100go.co/)
|
||||
|
||||
## Embedded Frontend
|
||||
|
||||
_upda_ includes a frontend in a monorepo fashion inside `server/web/`. For production (binary and OCI), it's
|
||||
embedded into the GoLang binary itself.
|
||||
|
||||
For _development_, no other steps are required. Simply follow the [frontend instructions](./server/web/README.md) and
|
||||
start the frontend separately.
|
||||
|
||||
If you like to have a look on the _production_ experience, the frontend needs to be build first and you need to build
|
||||
the Golang binary with `-tags prod`. How to properly build the frontend, please look into `build-web` of
|
||||
the `Makefile` (additional `rm -rf` cmd).
|
||||
|
||||
### Getting started
|
||||
|
||||
Ensure to set the following environment variables for proper debug logs during development
|
||||
|
||||
```shell
|
||||
DEVELOPMENT=true
|
||||
|
@ -20,25 +57,6 @@ LOGGING_ENCODING=console
|
|||
LOGGING_LEVEL=debug
|
||||
```
|
||||
|
||||
* 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
|
||||
|
@ -70,6 +88,34 @@ path.
|
|||
For any `go` command you run, ensure that your `PATH` has the `gcc` binary and that you add `CGO_ENABLED=1` as
|
||||
environment.
|
||||
|
||||
### Using the `lockService` correctly
|
||||
|
||||
The `lockService` can be used to lock resources. This works in-memory and also in a distributed fashion with REDIS.
|
||||
|
||||
Ensure to provide proper locking options when using, although in-memory ignores those.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
# invoked from an endpoint
|
||||
context := c.Request.Context()
|
||||
|
||||
var err error
|
||||
var lock appLock
|
||||
|
||||
if lock, err = h.lockService.lockWithOptions(context, "TEST-LOCK", withAppLockOptionExpiry(5*time.Minute), withAppLockOptionInfiniteRetries(), withAppLockOptionRetryDelay(5*time.Second)); err != nil {
|
||||
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||
return
|
||||
}
|
||||
# defer to avoid leakage
|
||||
defer func(lock appLock) {
|
||||
_ = lock.unlock(context)
|
||||
}(lock)
|
||||
|
||||
# simulate long running task
|
||||
time.Sleep(20 * time.Second)
|
||||
```
|
||||
|
||||
### Release
|
||||
|
||||
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
||||
|
@ -77,7 +123,7 @@ platform, uploads produced artifacts to that release and publishes docker images
|
|||
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
||||
built successfully:
|
||||
|
||||
* Adapt `constants_app.go` and change `Version` to the correct version number
|
||||
* Adapt `commons/constants.go` and change `Version` to the correct version number
|
||||
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||
in footer (link to scm git tag source)
|
||||
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
||||
|
@ -85,7 +131,7 @@ built successfully:
|
|||
|
||||
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||
|
||||
* Adapt `constants_app.go` and change `Version` to the _next_ version number
|
||||
* Adapt `commons/constants.go` and change `Version` to the _next_ version number
|
||||
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
||||
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
||||
|
|
63
_doc/Concepts.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Concepts, a deeper dive
|
||||
|
||||
The following section goes into a deeper look into upda's internals.
|
||||
|
||||
1. Create a webhook in upda.
|
||||
2. Use the webhook's URL in a 3rd party application to start tracking an update or use `upda-cli` to report an update.
|
||||
3. Enjoy visualization and state management of tracked updates in one place.
|
||||
4. Optionally, define _Actions_ for tracked updates as they arrive
|
||||
|
||||
_upda_ retrieves new updates when webhooks of upda are invoked, e.g., [duin](https://crazymax.dev/diun/) invokes it or
|
||||
any other application which can reach the instance. Tracked updates are unique for the
|
||||
attributes `(application,provider,host)` which means that subsequent updates for an identical _application_, _provider_
|
||||
and _host_ simply updates the `version` and `metadata` attributes for that tracked _update_ (regardless if the version
|
||||
or metadata payload _actually_ changed - reasoning behind this is to get reflected metadata updates independent if
|
||||
version attribute has changed).
|
||||
|
||||
State management of tracked updates:
|
||||
|
||||
* On first creation, state is set to _pending_.
|
||||
* When an _update_ is in _approved_ state, an invocation for it resets its state to _pending_.
|
||||
* _Ignored_ updates are skipped entirely and no attribute is updated.
|
||||
|
||||
##### The `application` attribute
|
||||
|
||||
The _application_ attribute is an arbitrary identifier, name or label of a subject you like to track,
|
||||
e.g., `docker.io/varakh/upda` for an OCI image.
|
||||
|
||||
##### The `provider` attribute
|
||||
|
||||
The _provider_ attribute is an arbitrary name or label. During webhook invocation the provider attribute is derived in
|
||||
priority:
|
||||
|
||||
For the _generic_ webhook:
|
||||
|
||||
1. If the incoming payload contains a non-blank `provider` attribute, it's taken from the request.
|
||||
2. If the incoming payload contains a blank or missing `provider` attribute, the issuing webhook's label is taken.
|
||||
|
||||
For the _diun_ webhook:
|
||||
|
||||
1. If the issuing webhook's label is blank, then `oci` is used.
|
||||
2. In any other case, the webhook's label is used.
|
||||
|
||||
Because the first priority is the issuing webhook's label, setting the _same_ label for all webhooks results in a
|
||||
grouping. Also see the _ignore host_ setting for `host` below.
|
||||
|
||||
_Remember that changing a webhook's label won't be reflected in already created/tracked updates!_
|
||||
|
||||
##### The `host` attribute
|
||||
|
||||
_host_ should be set to the originating host name a webhook has been issued from. The _host_
|
||||
attribute can also be "ignored" (a setting in each webhook). If set to ignored, _upda_ sets _host_ to _global_, thus
|
||||
update versions can be grouped independent of the originating host. If set for all webhooks, you'll end up with a host
|
||||
independent update dashboard.
|
||||
|
||||
##### The `version` attribute
|
||||
|
||||
The _version_ attribute is an arbitrary name or label and subject to change across invocations of webhooks. This can be
|
||||
a version number, a number of total updates, anything.
|
||||
|
||||
##### The `metadata` attribute
|
||||
|
||||
An update can hold any additional metadata information provided by request payload `metadata`. Metadata can be inspected
|
||||
via web interface or API.
|
74
_doc/Configuration.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
# Configuration
|
||||
|
||||
The following table describe available configuration values.
|
||||
|
||||
| Variable | Purpose | Default/Description |
|
||||
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `SECRET` | A 32 character long secure random secret used for encrypting some data inside the database. When data has been created inside the database, the secret cannot be changed anymore, otherwise decryption fails. | Not set by default, you need to explicitly set it, e.g., generate via `openssl rand -hex 16` |
|
||||
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
|
||||
| | | |
|
||||
| `AUTH_MODE` | The auth mode. Possible values are `basic_single` and `basic_credentials` | Defaults to `basic_single` |
|
||||
| `BASIC_AUTH_USER` | For auth mode `basic_single`: Username for login | Not set by default, you need to explicitly set it to user name |
|
||||
| `BASIC_AUTH_PASSWORD` | For auth mode `basic_single`: User's password for login | Not set by default, you need to explicitly set it to a secure random |
|
||||
| `BASIC_AUTH_CREDENTIALS` | For auth mode `basic_credentials`: list of comma separated credentials, e.g. `username1=password1,username2=password2` | Not set by default, you need to explicitly set it |
|
||||
| | | |
|
||||
| `DB_TYPE` | The database type (Postgres is **recommended**) | Defaults to `sqlite`, possible values are `sqlite` or `postgres` |
|
||||
| `DB_SQLITE_FILE` | Path to the SQLITE file | Defaults to `<XDG_DATA_DIR>/upda/upda.db`, e.g. `~/.local/share/upda/upda.db` |
|
||||
| `DB_POSTGRES_HOST` | The postgres host | Postgres host address, defaults to `localhost` |
|
||||
| `DB_POSTGRES_PORT` | The postgres port | Postgres port, defaults to `5432` |
|
||||
| `DB_POSTGRES_NAME` | The postgres database name | Postgres database name, needs to be set |
|
||||
| `DB_POSTGRES_TZ` | The postgres time zone | Postgres time zone settings, defaults to `Europe/Berlin` |
|
||||
| `DB_POSTGRES_USER` | The postgres user | Postgres user name, needs to be set |
|
||||
| `DB_POSTGRES_PASSWORD` | The postgres password | Postgres user password, needs to be set |
|
||||
| | | |
|
||||
| `SERVER_PORT` | Port | Defaults to `8080` |
|
||||
| `SERVER_LISTEN` | Server's listen address | Defaults to empty which equals `0.0.0.0` |
|
||||
| `SERVER_TLS_ENABLED` | If server uses TLS | Defaults `false` |
|
||||
| `SERVER_TLS_CERT_PATH` | When TLS enabled, provide the certificate path | |
|
||||
| `SERVER_TLS_KEY_PATH` | When TLS enabled, provide the key path | |
|
||||
| `SERVER_TIMEOUT` | Timeout the server waits before shutting down to end any pending tasks | Defaults to `1s` (1 second), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `CORS_ALLOW_ORIGINS` | CORS configuration | Defaults to `*` |
|
||||
| `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
|
||||
| `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` |
|
||||
| `CORS_ALLOW_CREDENTIALS` | CORS configuration | Defaults to `true` |
|
||||
| `CORS_EXPOSE_HEADERS` | CORS configuration | Defaults to `*` |
|
||||
| | | |
|
||||
| `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal`. Setting to `debug` enables high verbosity output. | Defaults to `info` |
|
||||
| `LOGGING_ENCODING` | Logging encoding. Possible are `console` and `json` | Defaults to `json` |
|
||||
| `LOGGING_DIRECTORY` | Logging directory. When set, logs will be added to a file called `upda.log` in addition to the standard output. Ensure that upda has access permissions. Use an external program for log rotation if desired. | |
|
||||
| | | |
|
||||
| `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number |
|
||||
| | | |
|
||||
| `TASK_UPDATE_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (ignored/approved) updates from the database | Defaults to `false` |
|
||||
| `TASK_UPDATE_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (ignored/approved) updates from the database | Defaults to `1h` (1 hour), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `TASK_UPDATE_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (ignored/approved) updates are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `720h` (168 hours = 1 week), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| | | |
|
||||
| `TASK_EVENT_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (old) events from the database | Defaults to `false` |
|
||||
| `TASK_EVENT_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (old) events from the database | Defaults to `8h` (8 hours), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `TASK_EVENT_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (old) events are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `2190h` (2190 hours = 3 months), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| | | |
|
||||
| `TASK_ACTIONS_ENQUEUE_ENABLED` | If background task should run to enqueue matching actions derived from events (actions are invocation separately after being enqueued) | Defaults to `true` |
|
||||
| `TASK_ACTIONS_ENQUEUE_INTERVAL` | Interval at which a background task does check to enqueue actions | Defaults to `10s` (10 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `TASK_ACTIONS_ENQUEUE_BATCH_SIZE` | Number defining how many unhandled events are processed in a batch by the background task | Defaults to `1`, must be positive number |
|
||||
| | | |
|
||||
| `TASK_ACTIONS_INVOKE_ENABLED` | If background task should run to invoke enqueued actions derived | Defaults to `true` |
|
||||
| `TASK_ACTIONS_INVOKE_INTERVAL` | Interval at which a background task does check to invoke enqueued actions | Defaults to `10s` (10 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `TASK_ACTIONS_INVOKE_BATCH_SIZE` | Number defining how many enqueued actions are processed in a batch by the background task | Defaults to `1`, must be positive number |
|
||||
| `TASK_ACTIONS_INVOKE_MAX_RETRIES` | Number defining how often actions are invoked in case of an error, if exceeded, those actions are not retried again | Defaults to `3`, must be positive number |
|
||||
| | | |
|
||||
| `TASK_ACTIONS_CLEAN_STALE_ENABLED` | If background task should run to do housekeeping of stale (handled, meaning success or error state) actions from the database | Defaults to `true` |
|
||||
| `TASK_ACTIONS_CLEAN_STALE_INTERVAL` | Interval at which a background task does housekeeping by deleting stale (handled) actions from the database | Defaults to `12h` (12 hours), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| `TASK_ACTIONS_CLEAN_STALE_MAX_AGE` | Number defining at which age stale (handled) actions are deleted by the background task (_updatedAt_ attribute decides) | Defaults to `720h` (720 hours = 30 days), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| | | |
|
||||
| `TASK_PROMETHEUS_REFRESH_INTERVAL` | Interval at which a background task updates custom metrics | Defaults to `60s` (60 seconds), qualifier can be `s = second`, `m = minute`, `h = hour` prefixed with a positive number |
|
||||
| | | |
|
||||
| `LOCK_REDIS_ENABLED` | If locking via REDIS (multiple instances) is enabled. Requires REDIS. Otherwise uses in-memory locks. | Defaults to `false` |
|
||||
| `LOCK_REDIS_URL` | If locking via REDIS is enabled, this should point to a resolvable REDIS instance, e.g. `redis://<user>:<pass>@localhost:6379/<db>`. | |
|
||||
| | | |
|
||||
| `PROMETHEUS_ENABLED` | If Prometheus metrics are exposed | Defaults to `false` |
|
||||
| `PROMETHEUS_METRICS_PATH` | Defines the metrics endpoint path | Defaults to `/metrics` |
|
||||
| `PROMETHEUS_SECURE_TOKEN_ENABLED` | If Prometheus metrics endpoint is protected by a token when enabled (**recommended**) | Defaults to `true` |
|
||||
| `PROMETHEUS_SECURE_TOKEN` | The token securing the metrics endpoint when enabled (**recommended**) | Not set by default, you need to explicitly set it to a secure random |
|
||||
| | | |
|
||||
| `WEB_API_URL` | Base URL of API, e.g. `https://upda.domain.tld` | `http://localhost` |
|
||||
| `WEB_TITLE` | The title of the frontend page | `upda` |
|
228
_doc/Deployment.md
Normal file
|
@ -0,0 +1,228 @@
|
|||
# Deployment
|
||||
|
||||
_upda_ is a server application which embeds a webinterface directly in its binary form. This makes it easy to deploy
|
||||
natively. In addition, a _upda_ docker image is provided to get started quickly.
|
||||
|
||||
_upda-cli_ which is an optional commandline helper to quickly invoke webhooks or list tracked updates in
|
||||
your is also embedded into the docker image, but can also be downloaded for your operating system.
|
||||
|
||||
The following sections outline how to deploy _upda_ in a containerized environment and also natively.
|
||||
|
||||
## Container
|
||||
|
||||
Use one of the provided `docker-compose` examples, edit to your needs. Then issue `docker compose up -d` command and
|
||||
`docker compose logs -f` to trace the log.
|
||||
|
||||
Default image user is `appuser` (`uid=2033`) and group is `appgroup` (`gid=2033`).
|
||||
|
||||
The following examples are available
|
||||
|
||||
### Postgres
|
||||
|
||||
#### docker-compose
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
internal:
|
||||
external: false
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.name: br-upda
|
||||
|
||||
services:
|
||||
app:
|
||||
container_name: upda_app
|
||||
image: git.myservermanager.com/varakh/upda:latest
|
||||
environment:
|
||||
- WEB_API_URL=https://upda.domain.tld
|
||||
- WEB_TITLE=upda
|
||||
- TZ=Europe/Berlin
|
||||
- DB_POSTGRES_TZ=Europe/Berlin
|
||||
- DB_TYPE=postgres
|
||||
- DB_POSTGRES_HOST=db
|
||||
- DB_POSTGRES_PORT=5432
|
||||
- DB_POSTGRES_NAME=upda
|
||||
- DB_POSTGRES_USER=upda
|
||||
- DB_POSTGRES_PASSWORD=upda
|
||||
- BASIC_AUTH_USER=admin
|
||||
- BASIC_AUTH_PASSWORD=changeit
|
||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||
- SECRET=generated-secure-secret-32-chars
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
container_name: upda_db
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=upda
|
||||
- POSTGRES_PASSWORD=upda
|
||||
- POSTGRES_DB=upda
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- upda-db-vol:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
upda-db-vol:
|
||||
external: false
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
#### docker-compose
|
||||
|
||||
You can use the following to get it up running quickly via docker compose.
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
internal:
|
||||
external: false
|
||||
driver: bridge
|
||||
driver_opts:
|
||||
com.docker.network.bridge.name: br-upda
|
||||
|
||||
services:
|
||||
app:
|
||||
container_name: upda_app
|
||||
image: git.myservermanager.com/varakh/upda:latest
|
||||
environment:
|
||||
- WEB_API_URL=https://upda.domain.tld
|
||||
- WEB_TITLE=upda
|
||||
- TZ=Europe/Berlin
|
||||
- BASIC_AUTH_USER=admin
|
||||
- BASIC_AUTH_PASSWORD=changeit
|
||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||
- SECRET=generated-secure-secret-32-chars
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- upda-app-vol:/home/appuser
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
|
||||
volumes:
|
||||
upda-app-vol:
|
||||
external: false
|
||||
```
|
||||
|
||||
#### Local example
|
||||
|
||||
For spinning it up **locally** and without a [reverse proxy](#reverse-proxy), you can use the following simple `docker`
|
||||
commands.
|
||||
|
||||
Make sure to adapt `DOMAIN` and pipe in your device IP address (LAN), e.g., `192.168.1.42`.
|
||||
|
||||
```shell
|
||||
# create volume
|
||||
docker volume create upda-app-vol
|
||||
|
||||
# run locally binding to your LAN IP address
|
||||
docker run --name upda_app \
|
||||
-p 8080:8080 \
|
||||
-e TZ=Europe/Berlin \
|
||||
-e WEB_API_URL=http://192.168.1.42:8080 \
|
||||
-e BASIC_AUTH_USER=admin \
|
||||
-e BASIC_AUTH_PASSWORD=changeit \
|
||||
-v upda-app-vol:/home/appuser \
|
||||
varakh/upda:latest
|
||||
```
|
||||
|
||||
## High availability
|
||||
|
||||
For high availability, pick the [Postgres setup](#postgres) and add [REDIS](https://redis.io/) to support proper
|
||||
distributed locking.
|
||||
|
||||
Make changes to your docker-compose deployment similar to the following:
|
||||
|
||||
```yaml
|
||||
# the existing app service - add these changes to all instances, so they all use the same redis instance
|
||||
# make sure that all of them can connect to the redis instance
|
||||
# ...
|
||||
app:
|
||||
environment:
|
||||
- LOCK_REDIS_ENABLED=true
|
||||
- LOCK_REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# the new redis service
|
||||
redis:
|
||||
container_name: upda_redis
|
||||
image: redis
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- internal
|
||||
volumes:
|
||||
- redis-data-vol:/var/redis/data
|
||||
# optionally expose port depending on your setup
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
|
||||
volumes:
|
||||
# other already defined volumes
|
||||
# ...
|
||||
redis-data-vol:
|
||||
external: false
|
||||
```
|
||||
|
||||
In addition, you need a proper load balancer which routes incoming traffic to all of your instances.
|
||||
|
||||
Furthermore, you can also decide to have the frontend in a high-availability setup.
|
||||
|
||||
## Reverse proxy
|
||||
|
||||
You may want to use a proxy in front of them on your host, e.g., nginx. Here's a configuration snippet which should do
|
||||
the work.
|
||||
|
||||
The UI and API (backend/server) is reachable through the same domain, e.g., `https://upda.domain.tld`. In addition,
|
||||
Let's Encrypt is used for transport encryption.
|
||||
|
||||
```shell
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
ssl_certificate /etc/letsencrypt/live/upda.domain.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/upda.domain.tld/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Native
|
||||
|
||||
Native deployment is also possible.
|
||||
|
||||
Download the binary for your operating system. Next, use the binary or execute it locally.
|
||||
|
||||
See the provided systemd service example `upda.service` to deploy on a UNIX/Linux machine.
|
||||
|
||||
```shell
|
||||
[Unit]
|
||||
Description=upda
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Using a dynamic user drops privileges and sets some security defaults
|
||||
# See https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
|
||||
DynamicUser=yes
|
||||
# All environment variables for upda can be put into this file
|
||||
# upda picks them up (on each restart)
|
||||
EnvironmentFile=/etc/upda.conf
|
||||
# Requires upda' binary to be installed at this location, e.g., via package manager or copying it over manually
|
||||
ExecStart=/usr/local/bin/upda-server
|
||||
```
|
||||
|
||||
For a full set of available configuration, look into the [Configuration](./Configuration.md) section. Furthermore,
|
||||
it's recommended to set up proper [Monitoring](./Monitoring.md).
|
47
_doc/Home.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# upda
|
||||
|
||||
**Up**date **Da**shboard (upda). A simple application to keep track of updates from different hosts and systems.
|
||||
|
||||
Managing various application or OCI container image updates can be a tedious task:
|
||||
|
||||
* A lot of hosts to operate with a lot of different applications being deployed
|
||||
* A lot of different OCI containers to watch for updated images
|
||||
* No convenient dashboard to see and manage all the available updates in one place
|
||||
|
||||
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to be called
|
||||
via a webhook call (created within _upda_) from other applications, such as a bash script, an
|
||||
application like [duin](https://crazymax.dev/diun/) or simply by using the `upda-cli`.
|
||||
|
||||
Please head over to the [Usage](./Usage.md) section for a quick _Getting Started_ once you've [deployed](./Deployment.md)
|
||||
_upda_.
|
||||
|
||||
The code is hosted here: [upda and CLI application including frontend](https://git.myservermanager.com/varakh/upda).
|
||||
|
||||
## Features
|
||||
|
||||
_upda_ manages a list of updates with attributes attached to it. For new updates to arrive, _upda_ needs to get them
|
||||
from an external source.
|
||||
For this, _upda_ allows to manage webhooks, which can be called with a unique URL from any other application or even a
|
||||
bash script so that upda retrieves these information.
|
||||
|
||||
_upda_'s main features include
|
||||
|
||||
* Managing [Updates](./Usage.md#manage-updates) by changing their state (pending, ignored, approved)
|
||||
* Managing [Webhooks](./Usage.md#getting-updates-in-via-webhooks) which allow to get information into _upda_ regarding Updates
|
||||
and their properties (like version) you like to track
|
||||
* Managing [Actions](./Usage.md#actions) which allow you to further process changes made to an Update (created, state
|
||||
changed, version
|
||||
changed,), basically allowing you to invoke other systems with the help
|
||||
of [shoutrrr](https://containrrr.dev/shoutrrr/)
|
||||
* View [past invocation of Actions](./Usage.md#history-of-actions)
|
||||
* Viewing [events](./Usage.md#see-what-has-changed) which allow you to see what has changed and how Updates
|
||||
* [Metrics exporter](./Monitoring.md) via prometheus
|
||||
|
||||
_upda_ is designed to be simple. Only supported authorization mechanism is basic.
|
||||
|
||||
## What it is not
|
||||
|
||||
_upda_ is **NOT** a scraper to watch docker registries or GitHub releases, it simply tracks and consolidates updates
|
||||
from different sources, but you need to feed in these information on your own, e.g., via Webhooks. If you like to watch
|
||||
GitHub releases, write a scraper and use `upda-cli` to report back to _upda_.
|
||||
|
2242
_doc/Monitoring.md
Normal file
174
_doc/Usage.md
Normal file
|
@ -0,0 +1,174 @@
|
|||
# Usage
|
||||
|
||||
Getting started in _upda_ is easy after it has been [deployed](./Deployment.md) successfully and is reachable through your
|
||||
browser.
|
||||
|
||||
![img](./img/updates.png)
|
||||
|
||||
## Login
|
||||
|
||||
Head over to the deployed _upda_ instance in your browser and login with your credentials.
|
||||
|
||||
![img](./img/login.png)
|
||||
|
||||
## Getting updates in via Webhooks
|
||||
|
||||
To get your first updates into _upda_, create a new Webhook. Webhooks are the central piece of how _upda_ gets notified
|
||||
about updates.
|
||||
|
||||
![img](./img/webhooks.png)
|
||||
|
||||
After you've created a new Webhook, you should see
|
||||
|
||||
* a unique _upda_ `URL` which serves as entrypoint of other 3rd party applications,
|
||||
e.g., `/api/v1/webhooks/<a unique identifier>` and
|
||||
* a corresponding `token` (write it down somewhere, you won't see it again after initial creation) for the URL which
|
||||
must be sent as `X-Webhook-Token` header when calling _upda_'s URL.
|
||||
|
||||
Next step is to make your 3rd party application use this webhook and bring in new updates into _upda_.
|
||||
|
||||
A good example is [duin](https://crazymax.dev/diun/), which is able to watch docker images for changes and updates. It
|
||||
can be configured with a config file
|
||||
and [diun's "notif" plugin supports calling external webhooks once a change is observed](https://crazymax.dev/diun/notif/webhook/).
|
||||
We just need to configure _upda_ as the receiving application in diun's configuration file.
|
||||
|
||||
```yaml
|
||||
notif:
|
||||
webhook:
|
||||
endpoint: https://upda.domain.tld/api/v1/webhooks/ee03cd9e-04d0-4c7f-9866-efe219c2501e
|
||||
method: POST
|
||||
headers:
|
||||
content-type: application/json
|
||||
X-Webhook-Token: <the token from webhook creation in upda>
|
||||
timeout: 10s
|
||||
```
|
||||
|
||||
Expected payload is derived from the _type_ of the webhook which has been created in _upda_.
|
||||
|
||||
In addition, a webhook in _upda_ can be set to ignore the host. Please read more on that in the [Concepts](./Concepts.md)
|
||||
section.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions can be used to invoke arbitrary third party tools when an _event_ occurs, e.g., an update has been created or
|
||||
modified. An action is triggered when its conditions are met, which means that the action's definition (event name,
|
||||
host, application, provider) fits the change which happend in _upda_.
|
||||
|
||||
Actions have types. Different types require different payload to set them up. [shoutrrr](#shoutrrr) is supported as
|
||||
action type, which can send notifications to a variety of services like Gotify, Ntfy, Teams, OpsGenie and many more.
|
||||
It in turn also support invoking calls to an external URL. This means you can have a stream of events being triggered
|
||||
when an update arrives in _upda_.
|
||||
|
||||
To create an Action, go to the Actions tab and click on _Create new action_. Enter the necessary information and
|
||||
consult the Action's type documentation if necessary.
|
||||
|
||||
![img](./img/actions.png)
|
||||
|
||||
Supported events for Actions are the following:
|
||||
|
||||
| Event name | Description |
|
||||
|:-------------------------|:--------------------------------------------------------------------|
|
||||
| `update_created` | An update has been created |
|
||||
| `update_updated` | An update has been updated (not necessarily its version attribute!) |
|
||||
| `update_updated_state` | An update's state changed |
|
||||
| `update_updated_version` | An update's version changed |
|
||||
| `update_deleted` | An update has been removed |
|
||||
|
||||
For privacy, an action's configuration supports upda's **secrets** vault, which means that before an action is
|
||||
triggered, any occurrence of `<SECRET>SECRET_KEY</SECRET>` is properly replaced by the value of the `SECRET_KEY` defined
|
||||
inside the vault.
|
||||
|
||||
Secrets can be used in all payload for an Action, including shoutrrr's URL. To create a new secret, go to the _Secrets_
|
||||
tab and click on _Create new secret_.
|
||||
|
||||
![img](./img/secrets.png)
|
||||
|
||||
In addition to secrets, upda provides **variables** which can be used with the `<VAR>VARIABLE_NAME</VAR>` syntax and any
|
||||
occurrence is replaced before invocation as well.
|
||||
|
||||
| Variable name | Description |
|
||||
|:-------------------------|:--------------------------------------------------|
|
||||
| `<VAR>APPLICATION</VAR>` | The update's application name invoking the action |
|
||||
| `<VAR>PROVIDER</VAR>` | The update's provider name invoking the action |
|
||||
| `<VAR>HOST</VAR>` | The update's host invoking the action |
|
||||
| `<VAR>VERSION</VAR>` | The update's version (latest) invoking the action |
|
||||
| `<VAR>STATE</VAR>` | The update's state invoking the action |
|
||||
|
||||
#### shoutrrr
|
||||
|
||||
[shoutrrr](https://github.com/containrrr/shoutrrr?tab=readme-ov-file#documentation) supports multiple services directly
|
||||
which can be provided as simple URL, e.g., `gotify://gotify.example.com:443/<token>`, where `<token>`
|
||||
can also be provided as secret: `gotify://gotify.example.com:443/<SECRET>GOTIFY_TOKEN</SECRET>`.
|
||||
|
||||
##### shoutrrr: example for sending mails
|
||||
|
||||
Before starting, add the following _Secrets_:
|
||||
|
||||
```
|
||||
MAIL_USER
|
||||
MAIL_PASS
|
||||
MAIL_HOST
|
||||
MAIL_PORT
|
||||
MAIL_FROM
|
||||
MAIL_TO
|
||||
```
|
||||
|
||||
For each event, now create a new _Action_ with different payload:
|
||||
|
||||
_New updates_
|
||||
|
||||
| Field | Content |
|
||||
|:------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+New+Update` |
|
||||
| Body | `New update '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) arrived on <VAR>HOST</VAR> for provider <VAR>PROVIDER</VAR>.` |
|
||||
|
||||
_Update changed_
|
||||
|
||||
| Field | Content |
|
||||
|:------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+changed` |
|
||||
| Body | `Update '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) changed on <VAR>HOST</VAR> for provider <VAR>PROVIDER</VAR>.` |
|
||||
|
||||
_Version changed_
|
||||
|
||||
| Field | Content |
|
||||
|:------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+version+changed` |
|
||||
| Body | `Update's version changed to '<VAR>VERSION</VAR>' for '<VAR>APPLICATION</VAR>' on <VAR>HOST</VAR> and provider <VAR>PROVIDER</VAR>.` |
|
||||
|
||||
_State changed_
|
||||
|
||||
| Field | Content |
|
||||
|:------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| URL 1 | `smtp://<SECRET>MAIL_USER</SECRET>:<SECRET>MAIL_PASS</SECRET>@<SECRET>MAIL_HOST</SECRET>:<SECRET>MAIL_PORT</SECRET>/?from=<SECRET>MAIL_FROM</SECRET>&to=<SECRET>MAIL_TO</SECRET>&Subject=[upda]+Update+state+changed` |
|
||||
| Body | `Update's state changed to '<VAR>STATE</VAR>' for '<VAR>APPLICATION</VAR>' (<VAR>VERSION</VAR>) on <VAR>HOST</VAR> and provider <VAR>PROVIDER</VAR>.` |
|
||||
|
||||
In addition, you can have multiple URL fields, e.g., for sending a mail and a push notification.
|
||||
|
||||
### History of actions
|
||||
|
||||
Whenever new updates come in, are changed or an update's state changes, _upda_ enqueues all matching Actions.
|
||||
|
||||
If you head over to the Action History tab, you see pending, currently running, successful or error invocations of
|
||||
actions.
|
||||
|
||||
![img](./img/actions_history.png)
|
||||
|
||||
## Manage updates
|
||||
|
||||
Once Update are in _upda_, you can filter them by state, application or other properties to only see pending Updates for
|
||||
example.
|
||||
|
||||
Furthermore, you can change their state to be ignored (see [Concepts](./Concepts.md)) or delete them.
|
||||
|
||||
![img](./img/updates.png)
|
||||
|
||||
In addition, you can view an Update's details by clicking on the small info icon for an Update.
|
||||
|
||||
![img](./img/updates_detail.png)
|
||||
|
||||
## See what has changed
|
||||
|
||||
For a full activity view, head over to the Events tab.
|
||||
|
||||
![img](./img/events.png)
|
|
@ -2,7 +2,7 @@ openapi: 3.0.3
|
|||
info:
|
||||
title: upda
|
||||
description: API specification
|
||||
version: 2.0.1
|
||||
version: 4.0.1
|
||||
externalDocs:
|
||||
description: Find out more about the project
|
||||
url: https://git.myservermanager.com/varakh/upda
|
||||
|
|
BIN
_doc/img/actions.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
_doc/img/actions_history.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
_doc/img/events.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
_doc/img/login.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
_doc/img/secrets.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
_doc/img/updates.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
_doc/img/updates_detail.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
_doc/img/webhooks.png
Normal file
After Width: | Height: | Size: 50 KiB |
|
@ -1,5 +1,15 @@
|
|||
package api
|
||||
|
||||
const (
|
||||
HeaderAppName = "X-App-Name"
|
||||
HeaderAppVersion = "X-App-Version"
|
||||
|
||||
HeaderWebhookToken = "X-Webhook-Token"
|
||||
|
||||
HeaderContentType = "Content-Type"
|
||||
HeaderContentTypeApplicationJson = "application/json"
|
||||
)
|
||||
|
||||
// UpdateState state of an update
|
||||
type UpdateState string
|
||||
|
||||
|
|
5
commons/constants.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package commons
|
||||
|
||||
const (
|
||||
Version = "4.0.1"
|
||||
)
|
74
go.mod
|
@ -1,62 +1,69 @@
|
|||
module git.myservermanager.com/varakh/upda
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.3
|
||||
|
||||
require (
|
||||
github.com/Depado/ginprom v1.8.1
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/adrg/xdg v0.5.1
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/gin-contrib/cors v1.7.1
|
||||
github.com/gin-contrib/zap v1.1.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
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.20.0
|
||||
github.com/go-resty/resty/v2 v2.12.0
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-contrib/static v1.1.2
|
||||
github.com/gin-contrib/zap v1.1.4
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1
|
||||
github.com/go-co-op/gocron/v2 v2.12.1
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/go-redsync/redsync/v4 v4.13.0
|
||||
github.com/go-resty/resty/v2 v2.15.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/urfave/cli/v2 v2.27.2
|
||||
github.com/urfave/cli/v2 v2.27.5
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.10
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
moul.io/zapgorm2 v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.3 // indirect
|
||||
github.com/bytedance/sonic v1.12.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-redsync/redsync/v4 v4.11.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-json v0.10.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/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
|
@ -66,14 +73,15 @@ require (
|
|||
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.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
golang.org/x/arch v0.9.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
226
go.sum
|
@ -8,8 +8,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
|
|||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA=
|
||||
github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/adrg/xdg v0.5.1 h1:Im8iDbEFARltY09yOJlSGu4Asjk2vF85+3Dyru8uJ0U=
|
||||
github.com/adrg/xdg v0.5.1/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
||||
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
|
@ -19,21 +19,19 @@ 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.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
|
||||
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
|
||||
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/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/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4=
|
||||
github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
|
@ -42,11 +40,8 @@ github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJ
|
|||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
|
||||
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -62,20 +57,22 @@ 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/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs=
|
||||
github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/zap v1.1.1 h1:DDyIF9YQorl3gZzAabIowRywHJuohDfiLnhwvWKl6SY=
|
||||
github.com/gin-contrib/zap v1.1.1/go.mod h1:YW8KOko2kYLy8g6k9YgVNTj7SIcrUEzYiAd9IjiBPs0=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
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.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/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
|
||||
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
|
||||
github.com/gin-contrib/zap v1.1.4 h1:xvxTybg6XBdNtcQLH3Tf0lFr4vhDkwzgLLrIGlNTqIo=
|
||||
github.com/gin-contrib/zap v1.1.4/go.mod h1:7lgEpe91kLbeJkwBTPgtVBy4zMa6oSBEcvj662diqKQ=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1 h1:xM+mzO88L+kODvY4vIUVLlZuyWazK5vJfK0DiFachdQ=
|
||||
github.com/go-co-op/gocron-redis-lock/v2 v2.0.1/go.mod h1:FSHZ13f4bfH37RpJi9l3vl2GTiJRUI6xTDbUvXLoqrY=
|
||||
github.com/go-co-op/gocron/v2 v2.12.1 h1:dCIIBFbzhWKdgXeEifBjHPzgQ1hoWhjS4289Hjjy1uw=
|
||||
github.com/go-co-op/gocron/v2 v2.12.1/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
|
@ -86,36 +83,33 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
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.11.0 h1:OPEcAxHBb95EzfwCKWM93ksOwHd5bTce2BD4+R14N6k=
|
||||
github.com/go-redsync/redsync/v4 v4.11.0/go.mod h1:ZfayzutkgeBmEmBlUR3j+rF6kN44UUGtEdfzhBFZTPc=
|
||||
github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA=
|
||||
github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0=
|
||||
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
|
||||
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
|
||||
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
|
||||
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -127,8 +121,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/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/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
|
@ -136,17 +132,17 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||
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/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/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.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/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=
|
||||
|
@ -164,8 +160,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
|
@ -191,9 +187,8 @@ github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/
|
|||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
|
||||
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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=
|
||||
|
@ -201,6 +196,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
|
@ -209,14 +206,12 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne
|
|||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||
github.com/redis/rueidis v1.0.19/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=
|
||||
|
@ -236,7 +231,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
|
@ -254,22 +248,16 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||
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/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
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/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
|
@ -280,79 +268,51 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
|||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
|
||||
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
|
||||
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -360,28 +320,24 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg=
|
||||
google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
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.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
||||
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
|
||||
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
|
||||
moul.io/zapgorm2 v1.3.0/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=
|
||||
|
|
|
@ -11,8 +11,12 @@
|
|||
"schedule": [
|
||||
"monthly"
|
||||
],
|
||||
// security
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"dependencyDashboardOSVVulnerabilitySummary": "all",
|
||||
// skip next alpine, see https://github.com/mattn/go-sqlite3/issues/1164
|
||||
"packageRules": [
|
||||
// oci
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"alpine"
|
||||
|
@ -23,25 +27,70 @@
|
|||
],
|
||||
"enabled": false
|
||||
},
|
||||
// go
|
||||
{
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
],
|
||||
"matchPackagePrefixes": [
|
||||
"github.com/go-co-op/gocron"
|
||||
],
|
||||
"groupName": "gocron"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"groupName": "all minor dependencies",
|
||||
"groupSlug": "all-minor-deps"
|
||||
"groupName": "GoLang: all minor dependencies",
|
||||
"groupSlug": "golang-all-minor-deps"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"patch"
|
||||
],
|
||||
"groupName": "all patch dependencies",
|
||||
"groupSlug": "all-patch-deps"
|
||||
"groupName": "GoLang: all patch dependencies",
|
||||
"groupSlug": "golang-all-patch-deps"
|
||||
},
|
||||
// node
|
||||
// GLOBAL: ignore @types/node major and minor (manual upgrade with pipeline required)
|
||||
{
|
||||
"matchManagers": [
|
||||
"npm"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"@types/node"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"major",
|
||||
"minor"
|
||||
],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"npm"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"groupName": "Node: all minor dependencies",
|
||||
"groupSlug": "node-all-minor-deps"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"npm"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"patch"
|
||||
],
|
||||
"groupName": "Node: all patch dependencies",
|
||||
"groupSlug": "node-all-patch-deps"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func (h *actionHandler) paginate(c *gin.Context) {
|
|||
}
|
||||
|
||||
var data []*api.ActionResponse
|
||||
data = make([]*api.ActionResponse, 0)
|
||||
data = make([]*api.ActionResponse, 0, len(actions))
|
||||
|
||||
for _, e := range actions {
|
||||
data = append(data, &api.ActionResponse{
|
||||
|
@ -225,6 +225,6 @@ func (h *actionHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ func (h *actionInvocationHandler) paginate(c *gin.Context) {
|
|||
}
|
||||
|
||||
var data []*api.ActionInvocationResponse
|
||||
data = make([]*api.ActionInvocationResponse, 0)
|
||||
data = make([]*api.ActionInvocationResponse, 0, len(actionInvocations))
|
||||
|
||||
for _, e := range actionInvocations {
|
||||
data = append(data, &api.ActionInvocationResponse{
|
||||
|
@ -97,6 +97,6 @@ func (h *actionInvocationHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"git.myservermanager.com/varakh/upda/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -13,6 +14,6 @@ func newAuthHandler() *authHandler {
|
|||
}
|
||||
|
||||
func (h *authHandler) login(c *gin.Context) {
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.myservermanager.com/varakh/upda/api"
|
||||
"git.myservermanager.com/varakh/upda/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
@ -19,8 +20,8 @@ func errAbortWithValidatorPayload(c *gin.Context, err error) {
|
|||
errorMap[key] = txt
|
||||
}
|
||||
|
||||
resErr := newServiceError(IllegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
resErr := newServiceError(illegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
||||
return
|
||||
}
|
||||
|
@ -29,17 +30,19 @@ func errToHttpStatus(err error) int {
|
|||
var e *serviceError
|
||||
switch {
|
||||
case errors.As(err, &e):
|
||||
if e.Status == IllegalArgument {
|
||||
if e.Status == illegalArgument {
|
||||
return http.StatusBadRequest
|
||||
} else if e.Status == Unauthorized {
|
||||
} else if e.Status == unauthorized {
|
||||
return http.StatusUnauthorized
|
||||
} else if e.Status == Forbidden {
|
||||
} else if e.Status == forbidden {
|
||||
return http.StatusForbidden
|
||||
} else if e.Status == NotFound {
|
||||
} else if e.Status == notFound {
|
||||
return http.StatusNotFound
|
||||
} else if e.Status == Conflict {
|
||||
} else if e.Status == methodNotAllowed {
|
||||
return http.StatusMethodNotAllowed
|
||||
} else if e.Status == conflict {
|
||||
return http.StatusConflict
|
||||
} else if e.Status == General {
|
||||
} else if e.Status == general {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
default:
|
||||
|
@ -57,7 +60,7 @@ func errCodeToStr(err error) string {
|
|||
return string(e.Status)
|
||||
}
|
||||
|
||||
return string(General)
|
||||
return string(general)
|
||||
}
|
||||
|
||||
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
||||
|
|
|
@ -29,7 +29,7 @@ func (h *eventHandler) window(c *gin.Context) {
|
|||
}
|
||||
|
||||
var data []*api.EventResponse
|
||||
data = make([]*api.EventResponse, 0)
|
||||
data = make([]*api.EventResponse, 0, len(events))
|
||||
|
||||
for _, e := range events {
|
||||
data = append(data, &api.EventResponse{
|
||||
|
@ -66,6 +66,6 @@ func (h *eventHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package server
|
|||
|
||||
import (
|
||||
"git.myservermanager.com/varakh/upda/api"
|
||||
"git.myservermanager.com/varakh/upda/commons"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -16,8 +17,8 @@ func newInfoHandler(a *appConfig) *infoHandler {
|
|||
|
||||
func (h *infoHandler) show(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||
"name": Name,
|
||||
"version": Version,
|
||||
"name": name,
|
||||
"Version": commons.Version,
|
||||
"timeZone": h.appConfig.timeZone,
|
||||
}})
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ func (h *secretHandler) getAll(c *gin.Context) {
|
|||
}
|
||||
|
||||
var data []*api.SecretResponse
|
||||
data = make([]*api.SecretResponse, 0)
|
||||
data = make([]*api.SecretResponse, 0, len(secrets))
|
||||
|
||||
for _, e := range secrets {
|
||||
data = append(data, &api.SecretResponse{
|
||||
|
@ -92,6 +92,6 @@ func (h *secretHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func (h *updateHandler) paginate(c *gin.Context) {
|
|||
|
||||
s, stateQueryContainsAtLeastOne := c.GetQueryArray("state")
|
||||
|
||||
var states []api.UpdateState
|
||||
states := make([]api.UpdateState, 0)
|
||||
if stateQueryContainsAtLeastOne {
|
||||
for _, state := range s {
|
||||
states = append(states, api.UpdateState(state))
|
||||
|
@ -101,6 +101,6 @@ func (h *updateHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func (h *webhookHandler) paginate(c *gin.Context) {
|
|||
}
|
||||
|
||||
var data []*api.WebhookResponse
|
||||
data = make([]*api.WebhookResponse, 0)
|
||||
data = make([]*api.WebhookResponse, 0, len(webhooks))
|
||||
|
||||
for _, e := range webhooks {
|
||||
data = append(data, &api.WebhookResponse{
|
||||
|
@ -125,6 +125,6 @@ func (h *webhookHandler) delete(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ func newWebhookInvocationHandler(i *webhookInvocationService, w *webhookService)
|
|||
}
|
||||
|
||||
func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
||||
tokenHeader := c.GetHeader(HeaderWebhookToken)
|
||||
tokenHeader := c.GetHeader(api.HeaderWebhookToken)
|
||||
webhookId := c.Param("id")
|
||||
|
||||
var w *Webhook
|
||||
|
@ -52,11 +52,11 @@ func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
|||
}
|
||||
break
|
||||
default:
|
||||
err = newServiceError(IllegalArgument, errors.New("no default handler for webhook type found"))
|
||||
err = newServiceError(illegalArgument, errors.New("no default handler for webhook type found"))
|
||||
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -3,27 +3,46 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"git.myservermanager.com/varakh/upda/api"
|
||||
"git.myservermanager.com/varakh/upda/commons"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func middlewareAppName() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header(HeaderAppName, Name)
|
||||
c.Header(api.HeaderAppName, name)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareGlobalNotFound() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, api.NewErrorResponseWithStatusAndMessage(string(notFound), "page not found"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareGlobalMethodNotAllowed() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, api.NewErrorResponseWithStatusAndMessage(string(methodNotAllowed), "method not allowed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareEnforceJsonContentType() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method != http.MethodOptions && !strings.HasPrefix(c.GetHeader(api.HeaderContentType), api.HeaderContentTypeApplicationJson) {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, api.NewErrorResponseWithStatusAndMessage(string(illegalArgument), "content-type must be application/json"))
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
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.Header(api.HeaderAppVersion, commons.Version)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +55,7 @@ func middlewareErrorHandler() gin.HandlerFunc {
|
|||
|
||||
if len(c.Errors) > 0 {
|
||||
// status -1 doesn't overwrite existing status code
|
||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||
c.Header(api.HeaderContentType, api.HeaderContentTypeApplicationJson)
|
||||
c.JSON(-1, api.NewErrorResponseWithStatusAndMessage(errCodeToStr(c.Errors.Last()), c.Errors.Last().Error()))
|
||||
return
|
||||
}
|
||||
|
@ -47,7 +66,7 @@ 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.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(general), fmt.Sprintf("%s", err)))
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
|
|
202
server/app.go
|
@ -6,8 +6,10 @@ import (
|
|||
"fmt"
|
||||
"git.myservermanager.com/varakh/upda/util"
|
||||
"github.com/gin-contrib/cors"
|
||||
ginstatic "github.com/gin-contrib/static"
|
||||
ginzap "github.com/gin-contrib/zap"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "go.uber.org/automaxprocs"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -30,16 +32,20 @@ func Start() {
|
|||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// app init (router, services, handlers)
|
||||
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)
|
||||
var err error
|
||||
|
||||
ps := newPrometheusService(router, env.prometheusConfig)
|
||||
|
||||
if env.prometheusConfig.enabled {
|
||||
prometheusService.init()
|
||||
router.Use(prometheusService.prometheus.Instrument())
|
||||
if err = ps.init(); err != nil {
|
||||
zap.L().Sugar().Fatalf("Prometheus service init failed: %s", err.Error())
|
||||
}
|
||||
router.Use(ps.prometheus.Instrument())
|
||||
}
|
||||
|
||||
updateRepo := newUpdateDbRepo(env.db)
|
||||
|
@ -49,51 +55,100 @@ func Start() {
|
|||
actionRepo := newActionDbRepo(env.db)
|
||||
actionInvocationRepo := newActionInvocationDbRepo(env.db)
|
||||
|
||||
lockService := newLockMemService()
|
||||
var ls lockService
|
||||
|
||||
eventService := newEventService(eventRepo)
|
||||
updateService := newUpdateService(updateRepo, eventService)
|
||||
webhookService := newWebhookService(webhookRepo, env.webhookConfig)
|
||||
webhookInvocationService := newWebhookInvocationService(webhookService, updateService, env.webhookConfig)
|
||||
if env.lockConfig.redisEnabled {
|
||||
var e error
|
||||
ls, e = newLockRedisService(env.lockConfig)
|
||||
|
||||
secretService := newSecretService(secretRepo)
|
||||
actionService := newActionService(actionRepo, eventService)
|
||||
actionInvocationService := newActionInvocationService(actionInvocationRepo, actionService, eventService, secretService)
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create lock service", zap.Error(e))
|
||||
}
|
||||
} else {
|
||||
ls = newLockMemService()
|
||||
}
|
||||
|
||||
taskService := newTaskService(updateService, eventService, webhookService, actionService, actionInvocationService, lockService, prometheusService, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig)
|
||||
taskService.init()
|
||||
taskService.start()
|
||||
es := newEventService(eventRepo)
|
||||
us := newUpdateService(updateRepo, es)
|
||||
ws := newWebhookService(webhookRepo, env.webhookConfig)
|
||||
wis := newWebhookInvocationService(ws, us, env.webhookConfig)
|
||||
|
||||
updateHandler := newUpdateHandler(updateService, env.appConfig)
|
||||
webhookHandler := newWebhookHandler(webhookService)
|
||||
webhookInvocationHandler := newWebhookInvocationHandler(webhookInvocationService, webhookService)
|
||||
eventHandler := newEventHandler(eventService)
|
||||
secretHandler := newSecretHandler(secretService)
|
||||
actionHandler := newActionHandler(actionService)
|
||||
actionInvocationHandler := newActionInvocationHandler(actionService, actionInvocationService)
|
||||
ss := newSecretService(secretRepo)
|
||||
as := newActionService(actionRepo, es)
|
||||
ais := newActionInvocationService(actionInvocationRepo, as, es, ss)
|
||||
|
||||
infoHandler := newInfoHandler(env.appConfig)
|
||||
healthHandler := newHealthHandler()
|
||||
authHandler := newAuthHandler()
|
||||
var ts *taskService
|
||||
|
||||
if ts, err = newTaskService(us, es, ws, as, ais, ls, ps, env.appConfig, env.taskConfig, env.lockConfig, env.prometheusConfig); err != nil {
|
||||
zap.L().Sugar().Fatalf("Task service creation failed: %v", err)
|
||||
}
|
||||
|
||||
if err = ts.init(); err != nil {
|
||||
zap.L().Sugar().Fatalf("Task service initialization failed: %v", err)
|
||||
}
|
||||
|
||||
ts.start()
|
||||
|
||||
uh := newUpdateHandler(us, env.appConfig)
|
||||
wh := newWebhookHandler(ws)
|
||||
wih := newWebhookInvocationHandler(wis, ws)
|
||||
eh := newEventHandler(es)
|
||||
sh := newSecretHandler(ss)
|
||||
ah := newActionHandler(as)
|
||||
aih := newActionInvocationHandler(as, ais)
|
||||
|
||||
ih := newInfoHandler(env.appConfig)
|
||||
hh := newHealthHandler()
|
||||
authH := newAuthHandler()
|
||||
|
||||
router.Use(middlewareAppName())
|
||||
router.Use(middlewareAppVersion())
|
||||
router.Use(middlewareAppContentType())
|
||||
router.Use(middlewareErrorHandler())
|
||||
router.Use(middlewareAppErrorRecoveryHandler())
|
||||
router.NoRoute(middlewareGlobalNotFound())
|
||||
router.NoMethod(middlewareGlobalMethodNotAllowed())
|
||||
|
||||
// in production mode, the frontend is embedded on / during compile time utilizing -tags prod
|
||||
// if the prod tag is missing, development setup is used and a dummy frontend is shown on /
|
||||
var targetPath string
|
||||
if env.appConfig.isDevelopment {
|
||||
targetPath = "web_dev"
|
||||
} else {
|
||||
targetPath = "web/build"
|
||||
}
|
||||
router.Use(ginstatic.Serve("/", ginstatic.EmbedFolder(embeddedFiles, targetPath)))
|
||||
|
||||
if !env.appConfig.isDevelopment {
|
||||
embeddedFrontendGroup := router.Group("/")
|
||||
embeddedFrontendGroup.GET("/conf/runtime-config.js", func(c *gin.Context) {
|
||||
config := `
|
||||
const runtime_config = Object.freeze({
|
||||
VITE_API_URL: '%s/api/v1/',
|
||||
VITE_APP_TITLE: '%s'
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'runtime_config', {
|
||||
value: runtime_config,
|
||||
writable: false
|
||||
});
|
||||
`
|
||||
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(fmt.Sprintf(config, env.webConfig.apiUrl, env.webConfig.title)))
|
||||
})
|
||||
}
|
||||
|
||||
router.Use(cors.New(cors.Config{
|
||||
AllowOrigins: env.serverConfig.corsAllowOrigin,
|
||||
AllowOrigins: env.serverConfig.corsAllowOrigins,
|
||||
AllowMethods: env.serverConfig.corsAllowMethods,
|
||||
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
||||
AllowCredentials: true,
|
||||
AllowCredentials: env.serverConfig.corsAllowCredentials,
|
||||
ExposeHeaders: env.serverConfig.corsExposeHeaders,
|
||||
}))
|
||||
|
||||
apiPublicGroup := router.Group("/api/v1")
|
||||
apiPublicGroup.GET("/health", healthHandler.show)
|
||||
apiPublicGroup.GET("/info", infoHandler.show)
|
||||
apiPublicGroup.GET("/health", hh.show)
|
||||
apiPublicGroup.GET("/info", ih.show)
|
||||
|
||||
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.execute)
|
||||
apiPublicGroup.POST("/webhooks/:id", middlewareEnforceJsonContentType(), wih.execute)
|
||||
|
||||
var authMethodHandler gin.HandlerFunc
|
||||
|
||||
|
@ -109,48 +164,47 @@ func Start() {
|
|||
|
||||
apiAuthGroup := router.Group("/api/v1", authMethodHandler)
|
||||
|
||||
apiAuthGroup.GET("/login", authHandler.login)
|
||||
apiAuthGroup.GET("/login", authH.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("/updates", uh.paginate)
|
||||
apiAuthGroup.GET("/updates/:id", uh.get)
|
||||
apiAuthGroup.PATCH("/updates/:id/state", middlewareEnforceJsonContentType(), uh.updateState)
|
||||
apiAuthGroup.DELETE("/updates/:id", uh.delete)
|
||||
|
||||
apiAuthGroup.GET("/webhooks", webhookHandler.paginate)
|
||||
apiAuthGroup.POST("/webhooks", webhookHandler.create)
|
||||
apiAuthGroup.GET("/webhooks/:id", webhookHandler.get)
|
||||
apiAuthGroup.PATCH("/webhooks/:id/label", webhookHandler.updateLabel)
|
||||
apiAuthGroup.PATCH("/webhooks/:id/ignore-host", webhookHandler.updateIgnoreHost)
|
||||
apiAuthGroup.DELETE("/webhooks/:id", webhookHandler.delete)
|
||||
apiAuthGroup.GET("/webhooks", wh.paginate)
|
||||
apiAuthGroup.POST("/webhooks", middlewareEnforceJsonContentType(), wh.create)
|
||||
apiAuthGroup.GET("/webhooks/:id", wh.get)
|
||||
apiAuthGroup.PATCH("/webhooks/:id/label", middlewareEnforceJsonContentType(), wh.updateLabel)
|
||||
apiAuthGroup.PATCH("/webhooks/:id/ignore-host", middlewareEnforceJsonContentType(), wh.updateIgnoreHost)
|
||||
apiAuthGroup.DELETE("/webhooks/:id", wh.delete)
|
||||
|
||||
apiAuthGroup.GET("/events", eventHandler.window)
|
||||
apiAuthGroup.GET("/events/:id", eventHandler.get)
|
||||
apiAuthGroup.DELETE("/events/:id", eventHandler.delete)
|
||||
apiAuthGroup.GET("/events", eh.window)
|
||||
apiAuthGroup.GET("/events/:id", eh.get)
|
||||
apiAuthGroup.DELETE("/events/:id", eh.delete)
|
||||
|
||||
apiAuthGroup.GET("/secrets", secretHandler.getAll)
|
||||
apiAuthGroup.GET("/secrets/:id", secretHandler.get)
|
||||
apiAuthGroup.POST("/secrets", secretHandler.create)
|
||||
apiAuthGroup.PATCH("/secrets/:id/value", secretHandler.updateValue)
|
||||
apiAuthGroup.DELETE("/secrets/:id", secretHandler.delete)
|
||||
apiAuthGroup.GET("/secrets", sh.getAll)
|
||||
apiAuthGroup.GET("/secrets/:id", sh.get)
|
||||
apiAuthGroup.POST("/secrets", middlewareEnforceJsonContentType(), sh.create)
|
||||
apiAuthGroup.PATCH("/secrets/:id/value", middlewareEnforceJsonContentType(), sh.updateValue)
|
||||
apiAuthGroup.DELETE("/secrets/:id", sh.delete)
|
||||
|
||||
apiAuthGroup.GET("/actions", actionHandler.paginate)
|
||||
apiAuthGroup.POST("/actions", actionHandler.create)
|
||||
apiAuthGroup.GET("/actions/:id", actionHandler.get)
|
||||
apiAuthGroup.PATCH("/actions/:id/label", actionHandler.updateLabel)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-event", actionHandler.updateMatchEvent)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-host", actionHandler.updateMatchHost)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-application", actionHandler.updateMatchApplication)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-provider", actionHandler.updateMatchProvider)
|
||||
apiAuthGroup.PATCH("/actions/:id/payload", actionHandler.updatePayload)
|
||||
apiAuthGroup.PATCH("/actions/:id/enabled", actionHandler.updateEnabled)
|
||||
apiAuthGroup.DELETE("/actions/:id", actionHandler.delete)
|
||||
apiAuthGroup.POST("/actions/:id/test", actionInvocationHandler.test)
|
||||
apiAuthGroup.GET("/actions", ah.paginate)
|
||||
apiAuthGroup.POST("/actions", middlewareEnforceJsonContentType(), ah.create)
|
||||
apiAuthGroup.GET("/actions/:id", ah.get)
|
||||
apiAuthGroup.PATCH("/actions/:id/label", middlewareEnforceJsonContentType(), ah.updateLabel)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-event", middlewareEnforceJsonContentType(), ah.updateMatchEvent)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-host", middlewareEnforceJsonContentType(), ah.updateMatchHost)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-application", middlewareEnforceJsonContentType(), ah.updateMatchApplication)
|
||||
apiAuthGroup.PATCH("/actions/:id/match-provider", middlewareEnforceJsonContentType(), ah.updateMatchProvider)
|
||||
apiAuthGroup.PATCH("/actions/:id/payload", middlewareEnforceJsonContentType(), ah.updatePayload)
|
||||
apiAuthGroup.PATCH("/actions/:id/enabled", middlewareEnforceJsonContentType(), ah.updateEnabled)
|
||||
apiAuthGroup.DELETE("/actions/:id", ah.delete)
|
||||
apiAuthGroup.POST("/actions/:id/test", middlewareEnforceJsonContentType(), aih.test)
|
||||
|
||||
apiAuthGroup.GET("/action-invocations", actionInvocationHandler.paginate)
|
||||
apiAuthGroup.GET("/action-invocations/:id", actionInvocationHandler.get)
|
||||
apiAuthGroup.DELETE("/action-invocations/:id", actionInvocationHandler.delete)
|
||||
apiAuthGroup.GET("/action-invocations", aih.paginate)
|
||||
apiAuthGroup.GET("/action-invocations/:id", aih.get)
|
||||
apiAuthGroup.DELETE("/action-invocations/:id", aih.delete)
|
||||
|
||||
// start server
|
||||
serverAddress := fmt.Sprintf("%s:%d", env.serverConfig.listen, env.serverConfig.port)
|
||||
srv := &http.Server{
|
||||
Addr: serverAddress,
|
||||
|
@ -158,34 +212,34 @@ func Start() {
|
|||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
var e error
|
||||
|
||||
if env.serverConfig.tlsEnabled {
|
||||
err = srv.ListenAndServeTLS(env.serverConfig.tlsCertPath, env.serverConfig.tlsKeyPath)
|
||||
e = srv.ListenAndServeTLS(env.serverConfig.tlsCertPath, env.serverConfig.tlsKeyPath)
|
||||
} else {
|
||||
err = srv.ListenAndServe()
|
||||
e = srv.ListenAndServe()
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
zap.L().Sugar().Fatalf("Application cannot be started: %v", err)
|
||||
if e != nil && !errors.Is(e, http.ErrServerClosed) {
|
||||
zap.L().Sugar().Fatalf("Application cannot be started: %v", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// 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)
|
||||
quit := make(chan os.Signal, 1)
|
||||
// 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()
|
||||
ts.stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), env.serverConfig.timeout)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
if err = srv.Shutdown(ctx); err != nil {
|
||||
zap.L().Sugar().Fatalf("Shutdown failed, exited directly: %v", err)
|
||||
}
|
||||
// catching ctx.Done() for configured timeout
|
||||
|
|
9
server/app_embedded_ui_dev.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
//go:build !prod
|
||||
// +build !prod
|
||||
|
||||
package server
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed web_dev
|
||||
var embeddedFiles embed.FS
|
9
server/app_embedded_ui_prod.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
//go:build prod
|
||||
// +build prod
|
||||
|
||||
package server
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed web/build/*
|
||||
var embeddedFiles embed.FS
|
|
@ -1,11 +0,0 @@
|
|||
package server
|
||||
|
||||
const (
|
||||
HeaderAppName = "X-App-Name"
|
||||
HeaderAppVersion = "X-App-Version"
|
||||
|
||||
HeaderWebhookToken = "X-Webhook-Token"
|
||||
|
||||
headerContentType = "Content-Type"
|
||||
headerContentTypeApplicationJson = "application/json"
|
||||
)
|
|
@ -1,6 +1,5 @@
|
|||
package server
|
||||
|
||||
const (
|
||||
Name = "upda"
|
||||
Version = "2.0.1"
|
||||
name = "upda"
|
||||
)
|
||||
|
|
|
@ -16,6 +16,12 @@ const (
|
|||
envTZ = "TZ"
|
||||
tzDefault = "Europe/Berlin"
|
||||
|
||||
envWebApiUrl = "WEB_API_URL"
|
||||
webApiUrlDefault = "http://localhost"
|
||||
|
||||
envWebTitle = "WEB_TITLE"
|
||||
webTitleDefault = "upda"
|
||||
|
||||
envAuthMode = "AUTH_MODE"
|
||||
authModeDefault = authModeBasicSingle
|
||||
authModeBasicSingle = "basic_single"
|
||||
|
@ -35,12 +41,16 @@ const (
|
|||
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"
|
||||
envCorsAllowOrigins = "CORS_ALLOW_ORIGINS"
|
||||
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
||||
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
||||
envCorsAllowCredentials = "CORS_ALLOW_CREDENTIALS"
|
||||
envCorsExposeHeaders = "CORS_EXPOSE_HEADERS"
|
||||
corsAllowOriginsDefault = "*"
|
||||
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
corsAllowHeadersDefault = "Authorization, Content-Type"
|
||||
corsAllowCredentialsDefault = "true"
|
||||
corsExposeHeadersDefault = "*"
|
||||
|
||||
dbTypeSqlite = "sqlite"
|
||||
dbTypePostgres = "postgres"
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// JSONMap defined JSON data type, need to implements driver.Valuer, sql.Scanner interface
|
||||
// JSONMap defined JSON data type, need to implement driver.Valuer, sql.Scanner interface
|
||||
type JSONMap map[string]interface {
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ func (m JSONMap) Value() (driver.Value, error) {
|
|||
return string(ba), err
|
||||
}
|
||||
|
||||
// Scan scan value into Jsonb, implements sql.Scanner interface
|
||||
// Scan value into JSONB, implements sql.Scanner interface
|
||||
func (m *JSONMap) Scan(val interface{}) error {
|
||||
if val == nil {
|
||||
*m = make(JSONMap)
|
||||
|
|
|
@ -26,16 +26,23 @@ type appConfig struct {
|
|||
isDebug bool
|
||||
}
|
||||
|
||||
type webConfig struct {
|
||||
title string
|
||||
apiUrl string
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
port int
|
||||
listen string
|
||||
tlsEnabled bool
|
||||
tlsCertPath string
|
||||
tlsKeyPath string
|
||||
timeout time.Duration
|
||||
corsAllowOrigin []string
|
||||
corsAllowMethods []string
|
||||
corsAllowHeaders []string
|
||||
port int
|
||||
listen string
|
||||
tlsEnabled bool
|
||||
tlsCertPath string
|
||||
tlsKeyPath string
|
||||
timeout time.Duration
|
||||
corsAllowCredentials bool
|
||||
corsAllowOrigins []string
|
||||
corsAllowMethods []string
|
||||
corsAllowHeaders []string
|
||||
corsExposeHeaders []string
|
||||
}
|
||||
|
||||
type authConfig struct {
|
||||
|
@ -47,22 +54,22 @@ type authConfig struct {
|
|||
|
||||
type taskConfig struct {
|
||||
updateCleanStaleEnabled bool
|
||||
updateCleanStaleInterval string
|
||||
updateCleanStaleInterval time.Duration
|
||||
updateCleanStaleMaxAge time.Duration
|
||||
eventCleanStaleEnabled bool
|
||||
eventCleanStaleInterval string
|
||||
eventCleanStaleInterval time.Duration
|
||||
eventCleanStaleMaxAge time.Duration
|
||||
actionsEnqueueEnabled bool
|
||||
actionsEnqueueInterval string
|
||||
actionsEnqueueInterval time.Duration
|
||||
actionsEnqueueBatchSize int
|
||||
actionsInvokeEnabled bool
|
||||
actionsInvokeInterval string
|
||||
actionsInvokeInterval time.Duration
|
||||
actionsInvokeBatchSize int
|
||||
actionsInvokeMaxRetries int
|
||||
actionsCleanStaleEnabled bool
|
||||
actionsCleanStaleInterval string
|
||||
actionsCleanStaleInterval time.Duration
|
||||
actionsCleanStaleMaxAge time.Duration
|
||||
prometheusRefreshInterval string
|
||||
prometheusRefreshInterval time.Duration
|
||||
}
|
||||
|
||||
type lockConfig struct {
|
||||
|
@ -83,6 +90,7 @@ type prometheusConfig struct {
|
|||
|
||||
type Environment struct {
|
||||
appConfig *appConfig
|
||||
webConfig *webConfig
|
||||
authConfig *authConfig
|
||||
serverConfig *serverConfig
|
||||
taskConfig *taskConfig
|
||||
|
@ -165,7 +173,9 @@ func bootstrapEnvironment() *Environment {
|
|||
}
|
||||
|
||||
zapLogger := zap.Must(zapConfig.Build())
|
||||
defer zapLogger.Sync()
|
||||
defer func(zapLogger *zap.Logger) {
|
||||
_ = zapLogger.Sync()
|
||||
}(zapLogger)
|
||||
zap.ReplaceGlobals(zapLogger)
|
||||
|
||||
// assign defaults from given environment variables and validate
|
||||
|
@ -179,6 +189,13 @@ func bootstrapEnvironment() *Environment {
|
|||
isDevelopment: isDevelopment,
|
||||
}
|
||||
|
||||
// web config
|
||||
var webC *webConfig
|
||||
webC = &webConfig{
|
||||
title: os.Getenv(envWebTitle),
|
||||
apiUrl: os.Getenv(envWebApiUrl),
|
||||
}
|
||||
|
||||
// server config
|
||||
var sc *serverConfig
|
||||
|
||||
|
@ -201,15 +218,17 @@ func bootstrapEnvironment() *Environment {
|
|||
}
|
||||
|
||||
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)},
|
||||
port: serverPort,
|
||||
timeout: serverTimeout,
|
||||
listen: os.Getenv(envServerListen),
|
||||
tlsEnabled: serverTlsEnabled,
|
||||
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
||||
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
||||
corsAllowCredentials: os.Getenv(envCorsAllowCredentials) == "true",
|
||||
corsExposeHeaders: []string{os.Getenv(envCorsExposeHeaders)},
|
||||
corsAllowOrigins: []string{os.Getenv(envCorsAllowOrigins)},
|
||||
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
||||
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
||||
}
|
||||
|
||||
authMode := os.Getenv(envAuthMode)
|
||||
|
@ -236,15 +255,15 @@ func bootstrapEnvironment() *Environment {
|
|||
// 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())
|
||||
}
|
||||
updateCleanStaleInterval := parseDuration(envTaskUpdateCleanStaleInterval)
|
||||
updateCleanStaleMaxAge := parseDuration(envTaskUpdateCleanStaleMaxAge)
|
||||
eventCleanStaleMaxAge := parseDuration(envTaskEventCleanStaleMaxAge)
|
||||
actionsCleanStaleMaxAge := parseDuration(envTaskActionsCleanStaleMaxAge)
|
||||
eventCleanStaleInterval := parseDuration(envTaskEventCleanStaleInterval)
|
||||
actionsEnqueueInterval := parseDuration(envTaskActionsEnqueueInterval)
|
||||
actionsInvokeInterval := parseDuration(envTaskActionsInvokeInterval)
|
||||
actionsCleanStaleInterval := parseDuration(envTaskActionsCleanStaleInterval)
|
||||
prometheusRefreshInterval := parseDuration(envTaskPrometheusRefreshInterval)
|
||||
|
||||
var actionsEnqueueBatchSize int
|
||||
if actionsEnqueueBatchSize, err = strconv.Atoi(os.Getenv(envTaskActionsEnqueueBatchSize)); err != nil {
|
||||
|
@ -270,29 +289,24 @@ func bootstrapEnvironment() *Environment {
|
|||
zap.L().Sugar().Fatalf("Invalid actions invoke max retries, must be a positive number.")
|
||||
}
|
||||
|
||||
var actionsCleanStaleMaxAge time.Duration
|
||||
if actionsCleanStaleMaxAge, errParse = time.ParseDuration(os.Getenv(envTaskActionsCleanStaleMaxAge)); errParse != nil {
|
||||
zap.L().Sugar().Fatalf("Could not parse max age for cleaning stale actions. Reason: %s", errParse.Error())
|
||||
}
|
||||
|
||||
tc = &taskConfig{
|
||||
updateCleanStaleEnabled: os.Getenv(envTaskUpdateCleanStaleEnabled) == "true",
|
||||
updateCleanStaleInterval: os.Getenv(envTaskUpdateCleanStaleInterval),
|
||||
updateCleanStaleInterval: updateCleanStaleInterval,
|
||||
updateCleanStaleMaxAge: updateCleanStaleMaxAge,
|
||||
eventCleanStaleEnabled: os.Getenv(envTaskEventCleanStaleEnabled) == "true",
|
||||
eventCleanStaleInterval: os.Getenv(envTaskEventCleanStaleInterval),
|
||||
eventCleanStaleInterval: eventCleanStaleInterval,
|
||||
eventCleanStaleMaxAge: eventCleanStaleMaxAge,
|
||||
actionsEnqueueEnabled: os.Getenv(envTaskActionsEnqueueEnabled) == "true",
|
||||
actionsEnqueueInterval: os.Getenv(envTaskActionsEnqueueInterval),
|
||||
actionsEnqueueInterval: actionsEnqueueInterval,
|
||||
actionsEnqueueBatchSize: actionsEnqueueBatchSize,
|
||||
actionsInvokeEnabled: os.Getenv(envTaskActionsInvokeEnabled) == "true",
|
||||
actionsInvokeInterval: os.Getenv(envTaskActionsInvokeInterval),
|
||||
actionsInvokeInterval: actionsInvokeInterval,
|
||||
actionsInvokeBatchSize: actionsInvokeBatchSize,
|
||||
actionsInvokeMaxRetries: actionsInvokeMaxRetries,
|
||||
actionsCleanStaleEnabled: os.Getenv(envTaskActionsCleanStaleEnabled) == "true",
|
||||
actionsCleanStaleInterval: os.Getenv(envTaskActionsCleanStaleInterval),
|
||||
actionsCleanStaleInterval: actionsCleanStaleInterval,
|
||||
actionsCleanStaleMaxAge: actionsCleanStaleMaxAge,
|
||||
prometheusRefreshInterval: os.Getenv(envTaskPrometheusRefreshInterval),
|
||||
prometheusRefreshInterval: prometheusRefreshInterval,
|
||||
}
|
||||
|
||||
var lc *lockConfig
|
||||
|
@ -328,7 +342,9 @@ func bootstrapEnvironment() *Environment {
|
|||
gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
|
||||
if isDebug && isDevelopment {
|
||||
gormZapLogger := zap.Must(zapConfig.Build())
|
||||
defer gormZapLogger.Sync()
|
||||
defer func(gormZapLogger *zap.Logger) {
|
||||
_ = gormZapLogger.Sync()
|
||||
}(gormZapLogger)
|
||||
gormLogger := zapgorm2.New(gormZapLogger)
|
||||
gormConfig = &gorm.Config{Logger: gormLogger}
|
||||
}
|
||||
|
@ -339,7 +355,7 @@ func bootstrapEnvironment() *Environment {
|
|||
if os.Getenv(envDbType) == dbTypeSqlite {
|
||||
if os.Getenv(envDbSqliteFile) == "" {
|
||||
var defaultDbFile string
|
||||
if defaultDbFile, err = xdg.DataFile(Name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
||||
if defaultDbFile, err = xdg.DataFile(name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
||||
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
||||
}
|
||||
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
||||
|
@ -388,6 +404,7 @@ func bootstrapEnvironment() *Environment {
|
|||
}
|
||||
|
||||
env := &Environment{appConfig: ac,
|
||||
webConfig: webC,
|
||||
authConfig: authC,
|
||||
serverConfig: sc,
|
||||
taskConfig: tc,
|
||||
|
@ -417,6 +434,10 @@ func bootstrapFromEnvironmentAndValidate() {
|
|||
// app
|
||||
setEnvKeyDefault(envTZ, tzDefault)
|
||||
|
||||
// web
|
||||
setEnvKeyDefault(envWebTitle, webTitleDefault)
|
||||
setEnvKeyDefault(envWebApiUrl, webApiUrlDefault)
|
||||
|
||||
// webhook
|
||||
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
||||
|
||||
|
@ -465,9 +486,11 @@ func bootstrapFromEnvironmentAndValidate() {
|
|||
setEnvKeyDefault(envServerPort, serverPortDefault)
|
||||
setEnvKeyDefault(envServerListen, serverListenDefault)
|
||||
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
||||
setEnvKeyDefault(envCorsAllowOrigin, corsAllowOriginDefault)
|
||||
setEnvKeyDefault(envCorsAllowOrigins, corsAllowOriginsDefault)
|
||||
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
||||
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
||||
setEnvKeyDefault(envCorsAllowCredentials, corsAllowCredentialsDefault)
|
||||
setEnvKeyDefault(envCorsExposeHeaders, corsExposeHeadersDefault)
|
||||
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
||||
}
|
||||
|
||||
|
@ -488,6 +511,17 @@ func setEnvKeyDefault(key string, defaultValue string) {
|
|||
}
|
||||
}
|
||||
|
||||
func parseDuration(envProperty string) time.Duration {
|
||||
var duration time.Duration
|
||||
var err error
|
||||
|
||||
if duration, err = time.ParseDuration(os.Getenv(envProperty)); err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not parse duration for '%s'. Reason: %s", envProperty, err.Error())
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
func parseBasicAuthCredentials(envProperty string) map[string]string {
|
||||
if envProperty == "" {
|
||||
zap.L().Sugar().Fatalln("Invalid env for parsing basic auth credentials")
|
||||
|
|
|
@ -6,43 +6,44 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
errorValidationNotEmpty = newServiceError(IllegalArgument, errors.New("assert: empty values are not allowed"))
|
||||
errorValidationNotBlank = newServiceError(IllegalArgument, errors.New("assert: blank values are not allowed"))
|
||||
errorValidationPageGreaterZero = newServiceError(IllegalArgument, errors.New("assert: page has to be greater 0"))
|
||||
errorValidationPageSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
||||
errorValidationLimitGreaterZero = newServiceError(IllegalArgument, errors.New("assert: limit has to be greater 0"))
|
||||
errorValidationSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: size has to be greater 0"))
|
||||
errorValidationMaxRetriesGreaterZero = newServiceError(IllegalArgument, errors.New("assert: max retries has to be greater 0"))
|
||||
errorValidationNotEmpty = newServiceError(illegalArgument, errors.New("assert: empty values are not allowed"))
|
||||
errorValidationNotBlank = newServiceError(illegalArgument, errors.New("assert: blank values are not allowed"))
|
||||
errorValidationPageGreaterZero = newServiceError(illegalArgument, errors.New("assert: page has to be greater 0"))
|
||||
errorValidationPageSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
||||
errorValidationLimitGreaterZero = newServiceError(illegalArgument, errors.New("assert: limit has to be greater 0"))
|
||||
errorValidationSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: size has to be greater 0"))
|
||||
errorValidationMaxRetriesGreaterZero = newServiceError(illegalArgument, errors.New("assert: max retries has to be greater 0"))
|
||||
|
||||
errorResourceNotFound = newServiceError(NotFound, errors.New("resource not found"))
|
||||
errorResourceAccessDenied = newServiceError(Forbidden, errors.New("resource access denied"))
|
||||
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
|
||||
type errorCode string
|
||||
|
||||
const (
|
||||
IllegalArgument ErrorCode = "IllegalArgument"
|
||||
Unauthorized ErrorCode = "Unauthorized"
|
||||
Forbidden ErrorCode = "Forbidden"
|
||||
NotFound ErrorCode = "NotFound"
|
||||
Conflict ErrorCode = "Conflict"
|
||||
General ErrorCode = "General"
|
||||
illegalArgument errorCode = "IllegalArgument"
|
||||
unauthorized errorCode = "Unauthorized"
|
||||
forbidden errorCode = "Forbidden"
|
||||
notFound errorCode = "NotFound"
|
||||
methodNotAllowed errorCode = "MethodNotAllowed"
|
||||
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 {
|
||||
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))
|
||||
return newServiceError(general, fmt.Errorf("database error: %w", error))
|
||||
}
|
||||
|
||||
type serviceError struct {
|
||||
Status ErrorCode
|
||||
Status errorCode
|
||||
Cause error
|
||||
}
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ func (r *actionInvocationDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time
|
|||
}
|
||||
|
||||
func translateActionInvocationState(state ...api.ActionInvocationState) []string {
|
||||
states := make([]string, 0)
|
||||
states := make([]string, 0, len(state))
|
||||
if len(state) > 0 {
|
||||
for _, s := range state {
|
||||
states = append(states, s.Value())
|
||||
|
|
|
@ -125,7 +125,7 @@ func (r *eventDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ...
|
|||
return 0, errorValidationNotEmpty
|
||||
}
|
||||
|
||||
states := make([]string, 0)
|
||||
states := make([]string, 0, len(state))
|
||||
for _, i := range state {
|
||||
states = append(states, i.Value())
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ func (r *updateDbRepo) deleteByUpdatedAtBeforeAndStates(time time.Time, state ..
|
|||
return 0, errorValidationNotEmpty
|
||||
}
|
||||
|
||||
states := make([]string, 0)
|
||||
states := make([]string, 0, len(state))
|
||||
for _, i := range state {
|
||||
states = append(states, i.Value())
|
||||
}
|
||||
|
@ -231,7 +231,7 @@ func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order st
|
|||
order = "desc"
|
||||
}
|
||||
|
||||
states := make([]string, 0)
|
||||
states := make([]string, 0, len(state))
|
||||
if len(state) > 0 {
|
||||
for _, s := range state {
|
||||
states = append(states, s.Value())
|
||||
|
@ -248,7 +248,7 @@ func (r *updateDbRepo) paginate(page int, pageSize int, orderBy string, order st
|
|||
func (r *updateDbRepo) count(searchTerm string, searchIn string, state ...api.UpdateState) (int64, error) {
|
||||
var c int64
|
||||
|
||||
states := make([]string, 0)
|
||||
states := make([]string, 0, len(state))
|
||||
if len(state) > 0 {
|
||||
for _, s := range state {
|
||||
states = append(states, s.Value())
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WebhookRepository interface {
|
||||
type webhookRepository interface {
|
||||
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
||||
count() (int64, error)
|
||||
find(id string) (*Webhook, error)
|
||||
|
|
|
@ -39,7 +39,7 @@ func (s *actionService) create(label string, t api.ActionType, matchEvent *strin
|
|||
}
|
||||
|
||||
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||
return nil, newServiceError(IllegalArgument, validationErr)
|
||||
return nil, newServiceError(illegalArgument, validationErr)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -197,7 +197,7 @@ func (s *actionService) updateTypeAndPayload(id string, t api.ActionType, payloa
|
|||
}
|
||||
|
||||
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||
return nil, newServiceError(IllegalArgument, validationErr)
|
||||
return nil, newServiceError(illegalArgument, validationErr)
|
||||
}
|
||||
|
||||
if e, err = s.repo.updateTypeAndPayload(id, t, payload); err != nil {
|
||||
|
|
|
@ -28,7 +28,7 @@ func newActionInvocationService(r ActionInvocationRepository, a *actionService,
|
|||
|
||||
func (s *actionInvocationService) enqueue(batchSize int) error {
|
||||
if batchSize <= 0 {
|
||||
return newServiceError(General, errors.New("cannot enqueue actions from events with invalid configured batch size"))
|
||||
return newServiceError(general, errors.New("cannot enqueue actions from events with invalid configured batch size"))
|
||||
}
|
||||
|
||||
var events []*Event
|
||||
|
@ -54,7 +54,7 @@ func (s *actionInvocationService) enqueue(batchSize int) error {
|
|||
|
||||
func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Action) error {
|
||||
if event == nil || actions == nil {
|
||||
return newServiceError(IllegalArgument, errorValidationNotEmpty)
|
||||
return newServiceError(illegalArgument, errorValidationNotEmpty)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -65,7 +65,8 @@ func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Acti
|
|||
return err
|
||||
}
|
||||
|
||||
var filteredActions []*Action
|
||||
filteredActions := make([]*Action, 0)
|
||||
|
||||
for _, action := range actions {
|
||||
matchesEvent := action.MatchEvent == nil || *action.MatchEvent == event.Name
|
||||
matchesHost := action.MatchHost == nil || *action.MatchHost == eventPayload.Host
|
||||
|
@ -98,10 +99,10 @@ func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Acti
|
|||
|
||||
func (s *actionInvocationService) invoke(batchSize int, maxRetries int) error {
|
||||
if batchSize <= 0 {
|
||||
return newServiceError(General, errors.New("cannot invoke actions with invalid configured batch size"))
|
||||
return newServiceError(general, errors.New("cannot invoke actions with invalid configured batch size"))
|
||||
}
|
||||
if maxRetries <= 0 {
|
||||
return newServiceError(General, errors.New("cannot invoke actions with invalid configured max retries"))
|
||||
return newServiceError(general, errors.New("cannot invoke actions with invalid configured max retries"))
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -197,14 +198,14 @@ func (s *actionInvocationService) execute(action *Action, eventPayloadInfo *even
|
|||
var bytes []byte
|
||||
|
||||
if bytes, err = action.Payload.MarshalJSON(); err != nil {
|
||||
return newServiceError(General, err)
|
||||
return newServiceError(general, err)
|
||||
}
|
||||
|
||||
switch action.Type {
|
||||
case api.ActionTypeShoutrrr.Value():
|
||||
var payload actionPayloadShoutrrrDto
|
||||
if payload, err = util.UnmarshalGenericJSON[actionPayloadShoutrrrDto](bytes); err != nil {
|
||||
return newServiceError(General, err)
|
||||
return newServiceError(general, err)
|
||||
}
|
||||
|
||||
body := s.replaceVars(payload.Body, eventPayloadInfo)
|
||||
|
@ -219,7 +220,7 @@ func (s *actionInvocationService) execute(action *Action, eventPayloadInfo *even
|
|||
}
|
||||
break
|
||||
default:
|
||||
return newServiceError(General, errors.New("no matching action type found for invocation"))
|
||||
return newServiceError(general, errors.New("no matching action type found for invocation"))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -194,41 +194,41 @@ func (s *eventService) extractPayloadInfo(event *Event) (*eventPayloadInformatio
|
|||
var bytes []byte
|
||||
|
||||
if bytes, err = event.Payload.MarshalJSON(); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
|
||||
switch event.Name {
|
||||
case api.EventNameUpdateCreated.Value():
|
||||
var p api.EventPayloadUpdateCreatedDto
|
||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateCreatedDto](bytes); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||
case api.EventNameUpdateDeleted.Value():
|
||||
var p api.EventPayloadUpdateDeletedDto
|
||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateDeletedDto](bytes); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||
case api.EventNameUpdateUpdatedState.Value():
|
||||
var p api.EventPayloadUpdateUpdatedDto
|
||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||
case api.EventNameUpdateUpdatedVersion.Value():
|
||||
var p api.EventPayloadUpdateUpdatedDto
|
||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||
case api.EventNameUpdateUpdated.Value():
|
||||
var p api.EventPayloadUpdateUpdatedDto
|
||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||
return nil, newServiceError(General, err)
|
||||
return nil, newServiceError(general, err)
|
||||
}
|
||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||
}
|
||||
|
||||
return nil, newServiceError(General, errors.New("no matching event found"))
|
||||
return nil, newServiceError(general, errors.New("no matching event found"))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,65 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// lockService provides methods for locking resources, behavior depends on underlying implementation
|
||||
type lockService interface {
|
||||
init() error
|
||||
tryLock(resource string) error
|
||||
release(resource string) error
|
||||
exists(resource string) bool
|
||||
stop()
|
||||
// lock locks a resource applying default options (varies for implementations)
|
||||
lock(ctx context.Context, resource string) (appLock, error)
|
||||
|
||||
// lockWithOptions locks a resource with given options, not all options are applied (varies for implementations)
|
||||
lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error)
|
||||
}
|
||||
|
||||
type appLock interface {
|
||||
// unlock unlocks a lock
|
||||
unlock(ctx context.Context) error
|
||||
}
|
||||
|
||||
type appLockOption interface {
|
||||
apply(l *appLockOptions)
|
||||
}
|
||||
|
||||
type appLockOptionFunc func(o *appLockOptions)
|
||||
|
||||
func (f appLockOptionFunc) apply(o *appLockOptions) {
|
||||
f(o)
|
||||
}
|
||||
|
||||
type appLockOptions struct {
|
||||
expiry *time.Duration
|
||||
retryDelay *time.Duration
|
||||
maxRetries *int
|
||||
}
|
||||
|
||||
func withAppLockOptionExpiry(expiry time.Duration) appLockOption {
|
||||
return appLockOptionFunc(func(o *appLockOptions) {
|
||||
o.expiry = &expiry
|
||||
})
|
||||
}
|
||||
|
||||
func withAppLockOptionRetries(retries int) appLockOption {
|
||||
return appLockOptionFunc(func(o *appLockOptions) {
|
||||
o.maxRetries = &retries
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
appLockOptionMaxRetries = math.MaxInt32
|
||||
)
|
||||
|
||||
func withAppLockOptionInfiniteRetries() appLockOption {
|
||||
return appLockOptionFunc(func(o *appLockOptions) {
|
||||
o.maxRetries = &appLockOptionMaxRetries
|
||||
})
|
||||
}
|
||||
|
||||
func withAppLockOptionRetryDelay(retryDelay time.Duration) appLockOption {
|
||||
return appLockOptionFunc(func(o *appLockOptions) {
|
||||
o.retryDelay = &retryDelay
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,52 +1,75 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.myservermanager.com/varakh/upda/util"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
)
|
||||
|
||||
type lockMemService struct {
|
||||
registry *util.InMemoryLockRegistry
|
||||
}
|
||||
|
||||
var (
|
||||
errLockMemNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||
)
|
||||
|
||||
func newLockMemService() lockService {
|
||||
zap.L().Info("Initializing in-memory locking service")
|
||||
return &lockMemService{registry: util.NewInMemoryLockRegistry()}
|
||||
}
|
||||
|
||||
func (s *lockMemService) init() error {
|
||||
zap.L().Info("Initialized in-memory locking service")
|
||||
return nil
|
||||
// lock locks a given resource without any options (default expiration)
|
||||
func (s *lockMemService) lock(ctx context.Context, resource string) (appLock, error) {
|
||||
return s.lockWithOptions(ctx, resource, withAppLockOptionExpiry(0))
|
||||
}
|
||||
|
||||
func (s *lockMemService) tryLock(resource string) error {
|
||||
// lockWithOptions locks a given resource, only TTL as option is supported
|
||||
func (s *lockMemService) lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error) {
|
||||
if resource == "" {
|
||||
return errorValidationNotBlank
|
||||
return nil, errorValidationNotBlank
|
||||
}
|
||||
|
||||
var expiration time.Duration = 0
|
||||
if options != nil {
|
||||
lockOptions := &appLockOptions{}
|
||||
for _, o := range options {
|
||||
o.apply(lockOptions)
|
||||
}
|
||||
|
||||
if lockOptions.expiry != nil {
|
||||
expiration = *lockOptions.expiry
|
||||
}
|
||||
}
|
||||
|
||||
zap.L().Sugar().Debugf("Trying to lock '%s'", resource)
|
||||
s.registry.Lock(resource)
|
||||
|
||||
s.registry.LockWithTTL(resource, expiration)
|
||||
|
||||
zap.L().Sugar().Debugf("Locked '%s'", resource)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *lockMemService) release(resource string) error {
|
||||
if resource == "" {
|
||||
return errorValidationNotBlank
|
||||
l := &inMemoryLock{
|
||||
registry: s.registry,
|
||||
resource: resource,
|
||||
}
|
||||
|
||||
zap.L().Sugar().Debugf("Releasing lock '%s'", resource)
|
||||
err := s.registry.Unlock(resource)
|
||||
zap.L().Sugar().Debugf("Released lock '%s'", resource)
|
||||
|
||||
return err
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (s *lockMemService) exists(resource string) bool {
|
||||
return s.registry.Exists(resource)
|
||||
var _ appLock = (*inMemoryLock)(nil)
|
||||
|
||||
type inMemoryLock struct {
|
||||
registry *util.InMemoryLockRegistry
|
||||
resource string
|
||||
}
|
||||
|
||||
func (s *lockMemService) stop() {
|
||||
zap.L().Info("Clearing in-memory locking service")
|
||||
s.registry.Clear()
|
||||
func (r inMemoryLock) unlock(ctx context.Context) error {
|
||||
zap.L().Sugar().Debugf("Unlocking '%s'", r.resource)
|
||||
|
||||
if err := r.registry.Unlock(r.resource); err != nil {
|
||||
return errLockMemNotReleased
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
29
server/service_lock_mem_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
testLockName = "test_lock"
|
||||
)
|
||||
|
||||
func TestLockExpiresAndCannotBeReleased(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
s := newLockMemService()
|
||||
ctx := context.Background()
|
||||
|
||||
lock, lockErr := s.lockWithOptions(ctx, testLockName, withAppLockOptionExpiry(250*time.Millisecond))
|
||||
a.Nil(lockErr)
|
||||
a.NotNil(lock)
|
||||
|
||||
time.Sleep(251 * time.Millisecond)
|
||||
|
||||
unlockErr := lock.unlock(ctx)
|
||||
a.NotNil(unlockErr)
|
||||
a.ErrorContains(unlockErr, "could not release lock")
|
||||
}
|
109
server/service_lock_redis.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
redsyncgoredis "github.com/go-redsync/redsync/v4/redis/goredis/v9"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type lockRedisService struct {
|
||||
rs *redsync.Redsync
|
||||
}
|
||||
|
||||
var (
|
||||
errLockRedisNotObtained = newServiceError(conflict, errors.New("lock service: could not obtain lock"))
|
||||
errLockRedisNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||
)
|
||||
|
||||
func newLockRedisService(lc *lockConfig) (lockService, error) {
|
||||
zap.L().Info("Initializing REDIS locking service")
|
||||
|
||||
var err error
|
||||
var redisOptions *redis.Options
|
||||
redisOptions, err = redis.ParseURL(lc.redisUrl)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lock service: cannot parse REDIS URL '%s' to set up locking: %s", lc.redisUrl, err)
|
||||
}
|
||||
|
||||
c := redis.NewClient(redisOptions)
|
||||
if err = c.Ping(context.Background()).Err(); err != nil {
|
||||
return nil, fmt.Errorf("lock service: failed to connect to REDIS: %w", err)
|
||||
}
|
||||
|
||||
pool := redsyncgoredis.NewPool(c)
|
||||
rs := redsync.New(pool)
|
||||
|
||||
return &lockRedisService{rs: rs}, nil
|
||||
}
|
||||
|
||||
// lock locks a given resource without any options
|
||||
func (s *lockRedisService) lock(ctx context.Context, resource string) (appLock, error) {
|
||||
return s.lockWithOptions(ctx, resource, nil)
|
||||
}
|
||||
|
||||
// lockWithOptions locks a given resource considering all options
|
||||
func (s *lockRedisService) lockWithOptions(ctx context.Context, resource string, options ...appLockOption) (appLock, error) {
|
||||
if resource == "" {
|
||||
return nil, errorValidationNotBlank
|
||||
}
|
||||
|
||||
var rsOptions []redsync.Option
|
||||
|
||||
if options != nil {
|
||||
lockOptions := &appLockOptions{}
|
||||
for _, o := range options {
|
||||
o.apply(lockOptions)
|
||||
}
|
||||
|
||||
if lockOptions.expiry != nil {
|
||||
rsOptions = append(rsOptions, redsync.WithExpiry(*lockOptions.expiry))
|
||||
}
|
||||
if lockOptions.maxRetries != nil {
|
||||
rsOptions = append(rsOptions, redsync.WithTries(*lockOptions.maxRetries))
|
||||
}
|
||||
if lockOptions.retryDelay != nil {
|
||||
rsOptions = append(rsOptions, redsync.WithRetryDelay(*lockOptions.retryDelay))
|
||||
}
|
||||
}
|
||||
|
||||
mu := s.rs.NewMutex(resource, rsOptions...)
|
||||
|
||||
zap.L().Sugar().Debugf("Trying to lock '%s'", resource)
|
||||
|
||||
if err := mu.LockContext(ctx); err != nil {
|
||||
return nil, errLockRedisNotObtained
|
||||
}
|
||||
|
||||
zap.L().Sugar().Debugf("Locked '%s'", resource)
|
||||
|
||||
l := &redisLock{
|
||||
mu: mu,
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
var _ appLock = (*redisLock)(nil)
|
||||
|
||||
type redisLock struct {
|
||||
mu *redsync.Mutex
|
||||
}
|
||||
|
||||
func (r redisLock) unlock(ctx context.Context) error {
|
||||
zap.L().Sugar().Debugf("Unlocking '%s'", r.mu.Name())
|
||||
|
||||
unlocked, err := r.mu.UnlockContext(ctx)
|
||||
if err != nil {
|
||||
return errLockRedisNotReleased
|
||||
}
|
||||
if !unlocked {
|
||||
return errLockRedisNotReleased
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -3,7 +3,6 @@ package server
|
|||
import (
|
||||
"github.com/Depado/ginprom"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type prometheusService struct {
|
||||
|
@ -19,7 +18,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
|||
if c.secureTokenEnabled {
|
||||
p = ginprom.New(
|
||||
ginprom.Engine(r),
|
||||
ginprom.Namespace(Name),
|
||||
ginprom.Namespace(name),
|
||||
ginprom.Subsystem(""),
|
||||
ginprom.Path(c.path),
|
||||
ginprom.Ignore(c.path),
|
||||
|
@ -28,7 +27,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
|||
} else {
|
||||
p = ginprom.New(
|
||||
ginprom.Engine(r),
|
||||
ginprom.Namespace(Name),
|
||||
ginprom.Namespace(name),
|
||||
ginprom.Subsystem(""),
|
||||
ginprom.Ignore(c.path),
|
||||
ginprom.Path(c.path),
|
||||
|
@ -42,25 +41,34 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
|||
}
|
||||
}
|
||||
|
||||
func (s *prometheusService) init() {
|
||||
func (s *prometheusService) init() error {
|
||||
if !s.config.enabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
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.registerGaugeNoLabels(metricWebhooks, metricWebhooksHelp)
|
||||
err = s.registerGaugeNoLabels(metricEvents, metricEventsHelp)
|
||||
err = s.registerGaugeNoLabels(metricActions, metricActionsHelp)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Cannot initialize service. Reason: %v", err)
|
||||
if err := s.registerGaugeNoLabels(metricUpdatesTotal, metricUpdatesTotalHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricUpdatesPending, metricUpdatesPendingHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricUpdatesIgnored, metricUpdatesIgnoredHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricUpdatesApproved, metricUpdatesApprovedHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricWebhooks, metricWebhooksHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricEvents, metricEventsHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.registerGaugeNoLabels(metricActions, metricActionsHelp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *prometheusService) registerGaugeNoLabels(name string, help string) error {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.myservermanager.com/varakh/upda/api"
|
||||
"github.com/go-co-op/gocron"
|
||||
redislock "github.com/go-co-op/gocron-redis-lock"
|
||||
redislock "github.com/go-co-op/gocron-redis-lock/v2"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
|
@ -21,50 +23,62 @@ type taskService struct {
|
|||
taskConfig *taskConfig
|
||||
lockConfig *lockConfig
|
||||
prometheusConfig *prometheusConfig
|
||||
scheduler *gocron.Scheduler
|
||||
scheduler gocron.Scheduler
|
||||
}
|
||||
|
||||
const (
|
||||
taskLockNameUpdatesCleanStale = "updates_clean_stale"
|
||||
taskLockNameEventsCleanStale = "events_clean_stale"
|
||||
taskLockNameActionsEnqueue = "actions_enqueue"
|
||||
taskLockNameActionsInvoke = "actions_invoke"
|
||||
taskLockNameActionsCleanStale = "actions_clean_stale"
|
||||
taskLockNamePrometheusUpdate = "prometheus_update"
|
||||
jobUpdatesCleanStale = "UPDATES_CLEAN_STALE"
|
||||
jobEventsCleanStale = "EVENTS_CLEAN_STALE"
|
||||
jobActionsEnqueue = "ACTIONS_ENQUEUE"
|
||||
jobActionsInvoke = "ACTIONS_INVOKE"
|
||||
jobActionsCleanStale = "ACTIONS_CLEAN_STALE"
|
||||
jobPrometheusRefresh = "PROMETHEUS_REFRESH"
|
||||
)
|
||||
|
||||
var (
|
||||
initialTasksStartDelay = time.Now().Add(10 * time.Second)
|
||||
)
|
||||
|
||||
func newTaskService(u *updateService, e *eventService, w *webhookService, a *actionService, ai *actionInvocationService, l lockService, p *prometheusService, ac *appConfig, tc *taskConfig, lc *lockConfig, pc *prometheusConfig) *taskService {
|
||||
location, err := time.LoadLocation(ac.timeZone)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not initialize correct timezone for scheduler. Reason: %s", err.Error())
|
||||
func newTaskService(u *updateService, e *eventService, w *webhookService, a *actionService, ai *actionInvocationService, l lockService, p *prometheusService, ac *appConfig, tc *taskConfig, lc *lockConfig, pc *prometheusConfig) (*taskService, error) {
|
||||
var err error
|
||||
var location *time.Location
|
||||
if location, err = time.LoadLocation(ac.timeZone); err != nil {
|
||||
return nil, fmt.Errorf("could not initialize correct timezone for scheduler: %s", err)
|
||||
}
|
||||
|
||||
gocron.SetPanicHandler(func(jobName string, value any) {
|
||||
zap.L().Sugar().Errorf("Job '%s' had a panic %v", jobName, value)
|
||||
// global job options
|
||||
singletonModeOption := gocron.WithSingletonMode(gocron.LimitModeReschedule)
|
||||
errorEventListener := gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
|
||||
zap.L().Sugar().Errorf("Job '%s' (%v) had a panic %v", jobName, jobID, err)
|
||||
})
|
||||
successEventListener := gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
|
||||
zap.L().Sugar().Debugf("Job '%s' (%v) finished", jobName, jobID)
|
||||
})
|
||||
eventListenerOption := gocron.WithEventListeners(successEventListener, errorEventListener)
|
||||
startAtOption := gocron.WithStartAt(gocron.WithStartDateTime(initialTasksStartDelay))
|
||||
|
||||
scheduler := gocron.NewScheduler(location)
|
||||
// global scheduler options
|
||||
schedulerOptions := []gocron.SchedulerOption{gocron.WithLocation(location), gocron.WithGlobalJobOptions(singletonModeOption, eventListenerOption, startAtOption)}
|
||||
|
||||
if lc.redisEnabled {
|
||||
var redisOptions *redis.Options
|
||||
redisOptions, err = redis.ParseURL(lc.redisUrl)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Cannot parse REDIS URL '%s' to set up locking. Reason: %s", lc.redisUrl, err.Error())
|
||||
return nil, fmt.Errorf("cannot parse REDIS URL '%s' to set up locking for scheduler: %s", lc.redisUrl, err)
|
||||
}
|
||||
redisClient := redis.NewClient(redisOptions)
|
||||
locker, err := redislock.NewRedisLocker(redisClient, redislock.WithTries(1))
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Cannot set up REDIS locker. Reason: %s", err.Error())
|
||||
|
||||
var locker gocron.Locker
|
||||
if locker, err = redislock.NewRedisLocker(redisClient, redislock.WithTries(1), redislock.WithExpiry(30*time.Second), redislock.WithRetryDelay(5*time.Second)); err != nil {
|
||||
return nil, fmt.Errorf("cannot set up REDIS locker for scheduler: %s", err)
|
||||
}
|
||||
scheduler.WithDistributedLocker(locker)
|
||||
|
||||
schedulerOptions = append(schedulerOptions, gocron.WithDistributedLocker(locker))
|
||||
}
|
||||
|
||||
scheduler, _ := gocron.NewScheduler(schedulerOptions...)
|
||||
|
||||
return &taskService{
|
||||
updateService: u,
|
||||
eventService: e,
|
||||
|
@ -78,331 +92,251 @@ func newTaskService(u *updateService, e *eventService, w *webhookService, a *act
|
|||
lockConfig: lc,
|
||||
prometheusConfig: pc,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *taskService) init() {
|
||||
s.configureCleanupStaleUpdatesTask()
|
||||
s.configureCleanupStaleEventsTask()
|
||||
s.configureActionsEnqueueTask()
|
||||
s.configureActionsInvokeTask()
|
||||
s.configureCleanupStaleActionsTask()
|
||||
s.configurePrometheusRefreshTask()
|
||||
func (s *taskService) init() error {
|
||||
if err := s.configureCleanupStaleUpdatesTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.configureCleanupStaleEventsTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.configureActionsEnqueueTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.configureActionsInvokeTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.configureCleanupStaleActionsTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.configurePrometheusRefreshTask(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) stop() {
|
||||
zap.L().Sugar().Infof("Stopping %d periodic tasks...", len(s.scheduler.Jobs()))
|
||||
s.scheduler.Stop()
|
||||
s.lockService.stop()
|
||||
if err := s.scheduler.StopJobs(); err != nil {
|
||||
zap.L().Sugar().Warnf("Cannot stop periodic tasks. Reason: %v", err)
|
||||
}
|
||||
if err := s.scheduler.Shutdown(); err != nil {
|
||||
zap.L().Sugar().Warnf("Cannot shut down scheduler. Reason: %v", err)
|
||||
}
|
||||
zap.L().Info("Stopped all periodic tasks")
|
||||
}
|
||||
|
||||
func (s *taskService) start() {
|
||||
s.scheduler.StartAsync()
|
||||
s.scheduler.Start()
|
||||
zap.L().Sugar().Infof("Started %d periodic tasks", len(s.scheduler.Jobs()))
|
||||
}
|
||||
|
||||
func (s *taskService) configureCleanupStaleUpdatesTask() {
|
||||
func (s *taskService) configureCleanupStaleUpdatesTask() error {
|
||||
if !s.taskConfig.updateCleanStaleEnabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
_, err := s.scheduler.Every(s.taskConfig.updateCleanStaleInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNameUpdatesCleanStale
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.updateCleanStaleMaxAge)
|
||||
runnable := func() {
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.updateCleanStaleMaxAge)
|
||||
|
||||
var err error
|
||||
var c int64
|
||||
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, 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().Debug("No stale updates found to clean up")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for cleaning stale updates. Reason: %s", err.Error())
|
||||
if c > 0 {
|
||||
zap.L().Sugar().Infof("Cleaned up '%d' stale updates", c)
|
||||
} else {
|
||||
zap.L().Debug("No stale updates found to clean up")
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.updateCleanStaleInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobUpdatesCleanStale)); err != nil {
|
||||
return fmt.Errorf("could not create task for cleaning stale updates: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) configureCleanupStaleEventsTask() {
|
||||
func (s *taskService) configureCleanupStaleEventsTask() error {
|
||||
if !s.taskConfig.eventCleanStaleEnabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.scheduler.Every(s.taskConfig.eventCleanStaleInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNameEventsCleanStale
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
runnable := func() {
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.eventCleanStaleMaxAge)
|
||||
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.eventCleanStaleMaxAge)
|
||||
var err error
|
||||
var c int64
|
||||
|
||||
var err error
|
||||
var c int64
|
||||
if c, err = s.eventService.cleanStale(t, api.EventStateCreated, api.EventStateEnqueued); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up stale events older than %s (%s). Reason: %s", s.taskConfig.eventCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if c, err = s.eventService.cleanStale(t, api.EventStateCreated, api.EventStateEnqueued); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up stale events older than %s (%s). Reason: %s", s.taskConfig.eventCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if c > 0 {
|
||||
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
|
||||
} else {
|
||||
zap.L().Debug("No stale events found to clean up")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for cleaning stale events. Reason: %s", err.Error())
|
||||
if c > 0 {
|
||||
zap.L().Sugar().Infof("Cleaned up '%d' stale events", c)
|
||||
} else {
|
||||
zap.L().Debug("No stale events found to clean up")
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.eventCleanStaleInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobEventsCleanStale)); err != nil {
|
||||
return fmt.Errorf("could not create task for cleaning stale events: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) configureActionsEnqueueTask() {
|
||||
func (s *taskService) configureActionsEnqueueTask() error {
|
||||
if !s.taskConfig.actionsEnqueueEnabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.scheduler.Every(s.taskConfig.actionsEnqueueInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNameActionsEnqueue
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
|
||||
if err := s.actionInvocationService.enqueue(s.taskConfig.actionsEnqueueBatchSize); err != nil {
|
||||
zap.L().Sugar().Errorf("Could enqueue actions. Reason: %s", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for enqueueing actions. Reason: %s", err.Error())
|
||||
runnable := func() {
|
||||
if err := s.actionInvocationService.enqueue(s.taskConfig.actionsEnqueueBatchSize); err != nil {
|
||||
zap.L().Sugar().Errorf("Could enqueue actions. Reason: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.actionsEnqueueInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsEnqueue)); err != nil {
|
||||
return fmt.Errorf("could not create task for enqueueing actions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) configureActionsInvokeTask() {
|
||||
func (s *taskService) configureActionsInvokeTask() error {
|
||||
if !s.taskConfig.actionsInvokeEnabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.scheduler.Every(s.taskConfig.actionsInvokeInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNameActionsInvoke
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
|
||||
if err := s.actionInvocationService.invoke(s.taskConfig.actionsInvokeBatchSize, s.taskConfig.actionsInvokeMaxRetries); err != nil {
|
||||
zap.L().Sugar().Errorf("Could invoke actions. Reason: %s", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for invoking actions. Reason: %s", err.Error())
|
||||
runnable := func() {
|
||||
if err := s.actionInvocationService.invoke(s.taskConfig.actionsInvokeBatchSize, s.taskConfig.actionsInvokeMaxRetries); err != nil {
|
||||
zap.L().Sugar().Errorf("Could invoke actions. Reason: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.actionsInvokeInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsInvoke)); err != nil {
|
||||
return fmt.Errorf("could not create task for invoking actions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) configureCleanupStaleActionsTask() {
|
||||
func (s *taskService) configureCleanupStaleActionsTask() error {
|
||||
if !s.taskConfig.actionsCleanStaleEnabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
_, err := s.scheduler.Every(s.taskConfig.actionsCleanStaleInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNameActionsCleanStale
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.actionsCleanStaleMaxAge)
|
||||
runnable := func() {
|
||||
t := time.Now()
|
||||
t = t.Add(-s.taskConfig.actionsCleanStaleMaxAge)
|
||||
|
||||
var cError int64
|
||||
var err error
|
||||
var cError int64
|
||||
var err error
|
||||
|
||||
if cError, err = s.actionInvocationService.cleanStale(t, s.taskConfig.actionsInvokeMaxRetries, api.ActionInvocationStateError); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up error stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
if cError, err = s.actionInvocationService.cleanStale(t, s.taskConfig.actionsInvokeMaxRetries, api.ActionInvocationStateError); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up error stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var cSuccess int64
|
||||
if cSuccess, err = s.actionInvocationService.cleanStale(t, 0, api.ActionInvocationStateSuccess); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up success stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
var cSuccess int64
|
||||
if cSuccess, err = s.actionInvocationService.cleanStale(t, 0, api.ActionInvocationStateSuccess); err != nil {
|
||||
zap.L().Sugar().Errorf("Could not clean up success stale actions older than %s (%s). Reason: %s", s.taskConfig.actionsCleanStaleMaxAge, t, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c := cError + cSuccess
|
||||
if c > 0 {
|
||||
zap.L().Sugar().Infof("Cleaned up '%d' stale actions", c)
|
||||
} else {
|
||||
zap.L().Debug("No stale actions found to clean up")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for cleaning stale actions. Reason: %s", err.Error())
|
||||
c := cError + cSuccess
|
||||
if c > 0 {
|
||||
zap.L().Sugar().Infof("Cleaned up '%d' stale actions", c)
|
||||
} else {
|
||||
zap.L().Debug("No stale actions found to clean up")
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.actionsCleanStaleInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobActionsCleanStale)); err != nil {
|
||||
return fmt.Errorf("could not create task for cleaning stale actions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *taskService) configurePrometheusRefreshTask() {
|
||||
func (s *taskService) configurePrometheusRefreshTask() error {
|
||||
if !s.prometheusConfig.enabled {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.scheduler.Every(s.taskConfig.prometheusRefreshInterval).
|
||||
StartAt(initialTasksStartDelay).
|
||||
Do(func() {
|
||||
resource := taskLockNamePrometheusUpdate
|
||||
// distributed lock handled via gocron-redis-lock for tasks
|
||||
if !s.lockConfig.redisEnabled {
|
||||
// skip execution if lock already exists, wait otherwise
|
||||
if lockExists := s.lockService.exists(resource); lockExists {
|
||||
zap.L().Sugar().Debugf("Skipping task execution because task lock '%s' exists", resource)
|
||||
return
|
||||
}
|
||||
_ = s.lockService.tryLock(resource)
|
||||
defer func(lockService lockService, resource string) {
|
||||
err := lockService.release(resource)
|
||||
if err != nil {
|
||||
zap.L().Sugar().Warnf("Could not release task lock '%s'", resource)
|
||||
}
|
||||
}(s.lockService, resource)
|
||||
}
|
||||
runnable := func() {
|
||||
updates, updatesError := s.updateService.getAll()
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var pendingTotal int64
|
||||
var ignoredTotal int64
|
||||
var ackTotal int64
|
||||
for _, update := range updates {
|
||||
if api.UpdateStatePending.Value() == update.State {
|
||||
pendingTotal += 1
|
||||
} else if api.UpdateStateIgnored.Value() == update.State {
|
||||
ignoredTotal += 1
|
||||
} else if api.UpdateStateApproved.Value() == update.State {
|
||||
ackTotal += 1
|
||||
}
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
if api.UpdateStatePending.Value() == update.State {
|
||||
pendingTotal += 1
|
||||
} else if api.UpdateStateIgnored.Value() == update.State {
|
||||
ignoredTotal += 1
|
||||
} else if api.UpdateStateApproved.Value() == update.State {
|
||||
ackTotal += 1
|
||||
}
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// actions
|
||||
var actionsTotal int64
|
||||
var actionsError error
|
||||
actionsTotal, actionsError = s.actionService.count()
|
||||
if actionsError = s.prometheusService.setGaugeNoLabels(metricActions, float64(actionsTotal)); actionsError != nil {
|
||||
zap.L().Sugar().Errorf("Could not refresh actions prometheus metric. Reason: %s", actionsError.Error())
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
zap.L().Sugar().Fatalf("Could not create task for refreshing prometheus. Reason: %s", err.Error())
|
||||
var actionsTotal int64
|
||||
var actionsError error
|
||||
actionsTotal, actionsError = s.actionService.count()
|
||||
if actionsError = s.prometheusService.setGaugeNoLabels(metricActions, float64(actionsTotal)); actionsError != nil {
|
||||
zap.L().Sugar().Errorf("Could not refresh actions prometheus metric. Reason: %s", actionsError.Error())
|
||||
}
|
||||
}
|
||||
|
||||
scheduledJob := gocron.DurationJob(s.taskConfig.prometheusRefreshInterval)
|
||||
if _, err := s.scheduler.NewJob(scheduledJob, gocron.NewTask(runnable), gocron.WithName(jobPrometheusRefresh)); err != nil {
|
||||
return fmt.Errorf("could not create task for refreshing prometheus: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
)
|
||||
|
||||
type webhookService struct {
|
||||
repo WebhookRepository
|
||||
repo webhookRepository
|
||||
webhookConfig *webhookConfig
|
||||
}
|
||||
|
||||
func newWebhookService(r WebhookRepository, c *webhookConfig) *webhookService {
|
||||
func newWebhookService(r webhookRepository, c *webhookConfig) *webhookService {
|
||||
return &webhookService{
|
||||
repo: r,
|
||||
webhookConfig: c,
|
||||
|
@ -42,7 +42,7 @@ func (s *webhookService) create(label string, t api.WebhookType, ignoreHost bool
|
|||
var token string
|
||||
|
||||
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
|
||||
return nil, newServiceError(General, fmt.Errorf("token generation failed: %w", err))
|
||||
return nil, newServiceError(general, fmt.Errorf("token generation failed: %w", err))
|
||||
}
|
||||
|
||||
var e *Webhook
|
||||
|
|
2
server/web/.env
Normal file
|
@ -0,0 +1,2 @@
|
|||
VITE_API_URL=
|
||||
VITE_APP_TITLE=upda
|
2
server/web/.env.development
Normal file
|
@ -0,0 +1,2 @@
|
|||
VITE_API_URL=http://localhost:8080/api/v1/
|
||||
VITE_APP_TITLE='upda dev'
|
1
server/web/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
_doc/*.png filter=lfs diff=lfs merge=lfs -text
|
32
server/web/.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
/build*
|
||||
.run/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
ci/*
|
||||
!ci/.gitkeep
|
||||
|
||||
# production
|
||||
build/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.idea
|
||||
/hs_err_pid14224.log
|
2
server/web/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
engine-strict=true
|
||||
legacy-peer-deps=true
|
2
server/web/.nvmrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
lts/iron
|
||||
v20
|
7
server/web/.prettierignore
Normal file
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
storybook-static
|
||||
package-lock.json
|
||||
dist
|
||||
ci
|
||||
build
|
||||
public
|
9
server/web/.prettierrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"useTabs": true,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": true
|
||||
}
|
41
server/web/.stylelintrc
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||
"plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties", "stylelint-prettier"],
|
||||
"rules": {
|
||||
"comment-empty-line-before": null,
|
||||
"selector-class-pattern": null,
|
||||
"function-name-case": [
|
||||
"lower",
|
||||
{
|
||||
"ignoreFunctions": []
|
||||
}
|
||||
],
|
||||
"no-invalid-double-slash-comments": null,
|
||||
"no-descending-specificity": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"selector-pseudo-element-colon-notation": "single",
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.less"
|
||||
],
|
||||
"customSyntax": "postcss-less"
|
||||
}
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"**/*.json",
|
||||
"**/*.svg",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"node_modules/"
|
||||
]
|
||||
}
|
75
server/web/README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# upda-ui
|
||||
|
||||
Frontend for upda - **Up**date **Da**shboard in React, TypeScript, Redux.
|
||||
|
||||
The main git repository is hosted at
|
||||
_[https://git.myservermanager.com/varakh/upda-ui](https://git.myservermanager.com/varakh/upda-ui)_. Other repositories
|
||||
are mirrors and pull requests, issues, and planning are managed there.
|
||||
|
||||
Contributions are very welcome!
|
||||
|
||||
[Official documentation](https://git.myservermanager.com/varakh/upda-docs) is hosted in a separate git repository.
|
||||
|
||||
## Development & contribution
|
||||
|
||||
Contributions are very welcome!
|
||||
|
||||
### Prerequisites
|
||||
|
||||
It's probably worth checking out a node environment manager like [nvm manager](https://github.com/nvm-sh/nvm).
|
||||
|
||||
Required node and npm versions are outlined in the `package.json`.
|
||||
|
||||
### Setup instructions
|
||||
|
||||
Run `npm install` which should install all dependencies.
|
||||
|
||||
### Start
|
||||
|
||||
Use the `npm run start` command to start the development setup. Backend should be running.
|
||||
|
||||
### Configuration magic in docker
|
||||
|
||||
What about configuration? How does the pre-compiled set of html, js and css files know about the environment variables?
|
||||
|
||||
In contrast to manual build, the docker image allows dynamic override of configuration, but only those outlined in
|
||||
the `.env` file.
|
||||
|
||||
In production, all configuration values are dynamically generated inside the Docker image with a helper script
|
||||
called `docker-env.sh`:
|
||||
|
||||
1. During docker build a template `.env` file and the helper script are copied to the docker image
|
||||
2. Before the container's nginx is started, the helper script
|
||||
1. scans the `.env` file for known configuration variables and then
|
||||
2. adds their values to `conf/runtime-config.js` which is sourced inside the application in the
|
||||
immutable `window.runtime_config` object
|
||||
|
||||
During development, this `runtime-config.js` file is still loaded, but empty and thus the `getConfiguration()` ignores
|
||||
it and prefers values from the sourced `.env.development`.
|
||||
|
||||
This means that new environment variables need to be added to all `.env*` files!
|
||||
|
||||
### Release
|
||||
|
||||
Releases are handled by the SCM platform and pipeline. Creating a **new git tag**, creates a new release in the SCM
|
||||
platform, uploads produced artifacts to that release and publishes docker images automatically.
|
||||
**Before** doing so, please ensure that the **commit on `master`** has the **correct version settings** and has been
|
||||
built successfully:
|
||||
|
||||
* Adapt `package.json` and change `version` to the correct version number
|
||||
* Invoke `npm install` once which properly sets the version inside the lock file
|
||||
* Adapt language files, e.g., `en.json` and change `version` to the correct version number
|
||||
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||
in footer
|
||||
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml`
|
||||
|
||||
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||
|
||||
* Adapt `package.json` and change `version` to the _next_ version number (semantic versioning applied, so `patch`
|
||||
should bump patch level version and prepare branch for `develop` should bump minor or major version)
|
||||
* Invoke `npm install` for each of those branches which properly sets the version inside the lock file
|
||||
* Adapt language files, e.g., `en.json` and change `version` to the _next_ version number (semantic versioning
|
||||
applied, so `patch` should bump patch level version and prepare branch for `develop` should bump minor or major
|
||||
version)
|
||||
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
0
server/web/ci/.gitkeep
Normal file
132
server/web/eslint.config.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
import jsRecommendedLib from '@eslint/js';
|
||||
import typescriptPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import sonarjsPlugin from 'eslint-plugin-sonarjs';
|
||||
import testingLibPlugin from 'eslint-plugin-testing-library';
|
||||
import { fixupPluginRules } from '@eslint/compat';
|
||||
|
||||
// eslint.config.js
|
||||
export default [
|
||||
{
|
||||
files: ['**/styles/**', '**/__tests__/**', '**/*.test.tsx', '**/*.test.ts', '*.less', 'src/**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
parser: typescriptParser
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': fixupPluginRules(reactHooksPlugin),
|
||||
sonarjs: sonarjsPlugin,
|
||||
import: fixupPluginRules(importPlugin),
|
||||
'jsx-a11y': jsxA11yPlugin,
|
||||
'@typescript-eslint': typescriptPlugin,
|
||||
prettier: prettierPlugin,
|
||||
js: jsRecommendedLib,
|
||||
'testing-library': fixupPluginRules(testingLibPlugin)
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {},
|
||||
node: {
|
||||
paths: ['src'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx']
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
...jsRecommendedLib.configs.recommended.rules,
|
||||
...reactPlugin.configs.recommended.rules,
|
||||
...reactPlugin.configs['jsx-runtime'].rules,
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
...importPlugin.configs.recommended.rules,
|
||||
...jsxA11yPlugin.flatConfigs.recommended.rules,
|
||||
semi: 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'no-undef': 'off',
|
||||
'prefer-const': 'error',
|
||||
'testing-library/no-debugging-utils': 'warn',
|
||||
'testing-library/no-dom-import': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/jsx-uses-react': 'off',
|
||||
'no-console': 'warn',
|
||||
'no-duplicate-imports': 'warn',
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'sonarjs/cognitive-complexity': 'off',
|
||||
'sonarjs/elseif-without-else': 'off',
|
||||
'sonarjs/max-switch-cases': 'error',
|
||||
'sonarjs/no-all-duplicated-branches': 'error',
|
||||
'sonarjs/no-collapsible-if': 'error',
|
||||
'sonarjs/no-collection-size-mischeck': 'error',
|
||||
'sonarjs/no-duplicate-string': 'off',
|
||||
'sonarjs/no-duplicated-branches': 'error',
|
||||
'sonarjs/no-element-overwrite': 'error',
|
||||
'sonarjs/no-empty-collection': 'error',
|
||||
'sonarjs/no-extra-arguments': 'error',
|
||||
'sonarjs/no-gratuitous-expressions': 'error',
|
||||
'sonarjs/no-identical-conditions': 'error',
|
||||
'sonarjs/no-identical-expressions': 'error',
|
||||
'sonarjs/no-identical-functions': 'error',
|
||||
'sonarjs/no-ignored-return': 'error',
|
||||
'sonarjs/no-inverted-boolean-check': 'error',
|
||||
'sonarjs/no-nested-switch': 'error',
|
||||
'sonarjs/no-nested-template-literals': 'error',
|
||||
'sonarjs/no-one-iteration-loop': 'error',
|
||||
'sonarjs/no-redundant-boolean': 'error',
|
||||
'sonarjs/no-redundant-jump': 'error',
|
||||
'sonarjs/no-same-line-conditional': 'error',
|
||||
'sonarjs/no-small-switch': 'error',
|
||||
'sonarjs/no-unused-collection': 'error',
|
||||
'sonarjs/no-use-of-empty-return-value': 'error',
|
||||
'sonarjs/no-useless-catch': 'error',
|
||||
'sonarjs/non-existent-operator': 'error',
|
||||
'sonarjs/prefer-immediate-return': 'error',
|
||||
'sonarjs/prefer-object-literal': 'error',
|
||||
'sonarjs/prefer-single-boolean-return': 'error',
|
||||
'sonarjs/prefer-while': 'error',
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
alphabetize: {
|
||||
caseInsensitive: true,
|
||||
order: 'asc'
|
||||
},
|
||||
groups: [['builtin', 'external', 'index', 'sibling', 'parent', 'internal']],
|
||||
'newlines-between': 'always',
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '*.less',
|
||||
group: 'index',
|
||||
patternOptions: {
|
||||
matchBase: true
|
||||
},
|
||||
position: 'before'
|
||||
},
|
||||
{
|
||||
pattern: '*.json',
|
||||
group: 'index',
|
||||
patternOptions: {
|
||||
matchBase: true
|
||||
},
|
||||
position: 'after'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
18
server/web/index.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/Favicon16x16.png" sizes="16x16" />
|
||||
<link rel="icon" type="image/x-icon" href="/Favicon32x32.png" sizes="32x32" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<script type="module" src="/conf/runtime-config.js"></script>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>upda</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
28419
server/web/package-lock.json
generated
Normal file
116
server/web/package.json
Normal file
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"name": "upda-ui",
|
||||
"version": "4.0.1",
|
||||
"private": true,
|
||||
"author": "varakh@varakh.de",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20",
|
||||
"npm": "^10"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "vitest watch",
|
||||
"test:no-watch": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:ci": "vitest run --coverage --passWithNoTests",
|
||||
"format": "prettier --write ./src/**/*.{ts,tsx,less}",
|
||||
"format:check": "prettier --check ./src/**/*.{ts,tsx,less}",
|
||||
"lint": "eslint \"./src/**/*.{tsx,ts}\"",
|
||||
"lintfix": "eslint --fix \"./src/**/*.{tsx,ts}\"",
|
||||
"lint:style": "stylelint \"./src/**/*.less\"",
|
||||
"lint-style-fix": "stylelint \"./src/**/*.less\" --fix",
|
||||
"checkstyle": "npm run checkstyle:ts && npm run checkstyle:less && npm run checkstyle:format",
|
||||
"checkstyle:format": "npm run format:check",
|
||||
"checkstyle:ts": "eslint \"./src/**/*.{ts,tsx}\" -f checkstyle > ci/eslint.xml",
|
||||
"checkstyle:less": "stylelint \"./src/**/*.less\"",
|
||||
"clean": "npx --quiet rimraf build && npx --quiet rimraf node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.1",
|
||||
"@ant-design/pro-layout": "^7.21.1",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"antd": "^5.21.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"html-react-parser": "^5.1.18",
|
||||
"i18next": "^23.16.3",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"linkify-html": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"react": "^18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.1.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/register": "^7.25.9",
|
||||
"@eslint/compat": "^1.2.1",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
||||
"@typescript-eslint/parser": "^8.11.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitest/coverage-v8": "^2.1.3",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-formatter-checkstyle": "^8.40.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-disable": "^2.0.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.1",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-sonarjs": "^2.0.4",
|
||||
"eslint-plugin-testing-library": "^6.4.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"postcss-less": "^6.0.0",
|
||||
"postcss-markdown": "^1.2.0",
|
||||
"prettier": "^3.3.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"stylelint": "^16.10.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"stylelint-declaration-block-no-ignored-properties": "^2.8.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"stylelint-prettier": "^5.0.2",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-eslint2": "^5.0.1",
|
||||
"vite-plugin-stylelint": "^5.3.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.1.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
server/web/public/Favicon16x16.png
Normal file
After Width: | Height: | Size: 816 B |
BIN
server/web/public/Favicon32x32.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
7
server/web/public/conf/runtime-config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Note that Object.freeze is NOT recursive
|
||||
const runtime_config = Object.freeze({});
|
||||
|
||||
Object.defineProperty(window, 'runtime_config', {
|
||||
value: runtime_config,
|
||||
writable: false
|
||||
});
|
BIN
server/web/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
8
server/web/public/manifest.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"short_name": "",
|
||||
"name": "",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
server/web/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
15
server/web/src/App.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import getConfiguration from './getConfiguration';
|
||||
import AppRouter from './router/AppRouter';
|
||||
import { isDevelopment } from './utils/envHelper';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
if (isDevelopment()) {
|
||||
document.title = getConfiguration().VITE_APP_TITLE;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <AppRouter />;
|
||||
};
|
||||
export default App;
|
8
server/web/src/__mocks__/config.mock.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const mockConfig = {};
|
||||
|
||||
Object.defineProperty(window, 'runtime_config', {
|
||||
writable: true,
|
||||
value: mockConfig
|
||||
});
|
||||
|
||||
export { mockConfig };
|
4
server/web/src/__mocks__/react-i18next.mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: (): [(key: string) => string] => [(key: string): string => key]
|
||||
}));
|
||||
export {};
|
67
server/web/src/api/actionInvocationsApi.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import ActionInvocationFilterQueryParamNames from '../constants/api/actionInvocationFilterQueryParamNames';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { ActionInvocationsRequestParams, ActionInvocationSingleResponse, ActionInvocationsResponse } from '../types';
|
||||
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
const invalidatesTags = (
|
||||
results?: ActionInvocationsResponse | ActionInvocationSingleResponse | void,
|
||||
error?: FetchBaseQueryError
|
||||
) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [ApiTags.ActionInvocations] as any;
|
||||
};
|
||||
|
||||
export const actionInvocationsApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getActionInvocations: build.query<ActionInvocationsResponse, ActionInvocationsRequestParams>({
|
||||
query: ({ page, pageSize, order, orderBy }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (page) {
|
||||
params.append(ActionInvocationFilterQueryParamNames.PAGE, `${page}`);
|
||||
}
|
||||
if (pageSize) {
|
||||
params.append(ActionInvocationFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||
}
|
||||
if (order) {
|
||||
params.append(ActionInvocationFilterQueryParamNames.ORDER, `${order}`);
|
||||
}
|
||||
if (orderBy) {
|
||||
params.append(ActionInvocationFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||
}
|
||||
return { url: `action-invocations?${params.toString()}` };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.ActionInvocations, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.ActionInvocations, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
getActionInvocationById: build.query<ActionInvocationSingleResponse, { id: string }>({
|
||||
query: ({ id }) => ({ url: `action-invocations/${id}` }),
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data) {
|
||||
return [{ type: ApiTags.ActionInvocations, id: result.data.id }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
deleteActionInvocation: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `action-invocations/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetActionInvocationsQuery, useGetActionInvocationByIdQuery, useDeleteActionInvocationMutation } =
|
||||
actionInvocationsApi;
|
197
server/web/src/api/actionsApi.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import ActionFilterQueryParamNames from '../constants/api/actionFilterQueryParamNames';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import {
|
||||
ActionSingleResponse,
|
||||
ActionsRequestParams,
|
||||
ActionsResponse,
|
||||
ActionTestSingleResponse,
|
||||
CreateActionRequest,
|
||||
ModifyActionEnabledRequest,
|
||||
ModifyActionLabelRequest,
|
||||
ModifyActionMatchApplicationRequest,
|
||||
ModifyActionMatchEventRequest,
|
||||
ModifyActionMatchHostRequest,
|
||||
ModifyActionMatchProviderRequest,
|
||||
ModifyActionPayloadRequest,
|
||||
TestActionRequest
|
||||
} from '../types';
|
||||
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
const invalidatesTags = (results?: ActionsResponse | ActionSingleResponse | void, error?: FetchBaseQueryError) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [ApiTags.Actions] as any;
|
||||
};
|
||||
|
||||
export const actionsApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getActions: build.query<ActionsResponse, ActionsRequestParams>({
|
||||
query: ({ page, pageSize, order, orderBy }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (page) {
|
||||
params.append(ActionFilterQueryParamNames.PAGE, `${page}`);
|
||||
}
|
||||
if (pageSize) {
|
||||
params.append(ActionFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||
}
|
||||
if (order) {
|
||||
params.append(ActionFilterQueryParamNames.ORDER, `${order}`);
|
||||
}
|
||||
if (orderBy) {
|
||||
params.append(ActionFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||
}
|
||||
return { url: `actions?${params.toString()}` };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.Actions, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.Actions, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
getActionById: build.query<ActionSingleResponse, { id: string }>({
|
||||
query: ({ id }) => ({ url: `actions/${id}` }),
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data) {
|
||||
return [{ type: ApiTags.Actions, id: result.data.id }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
createAction: build.mutation<ActionSingleResponse, CreateActionRequest>({
|
||||
query: (body) => ({ url: 'actions', method: 'POST', body }),
|
||||
invalidatesTags
|
||||
}),
|
||||
testAction: build.mutation<ActionTestSingleResponse, { id: string; body: TestActionRequest }>({
|
||||
query: ({ id, body }) => ({ url: `actions/${id}/test`, method: 'POST', body })
|
||||
}),
|
||||
modifyLabelAction: build.mutation<ActionSingleResponse, { id: string; body: ModifyActionLabelRequest }>({
|
||||
query: ({ id, body }) => ({ url: `actions/${id}/label`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyMatchApplicationAction: build.mutation<
|
||||
ActionSingleResponse,
|
||||
{ id: string; body: ModifyActionMatchApplicationRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({
|
||||
url: `actions/${id}/match-application`,
|
||||
method: 'PATCH',
|
||||
body
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyMatchHostAction: build.mutation<
|
||||
ActionSingleResponse,
|
||||
{ id: string; body: ModifyActionMatchHostRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({
|
||||
url: `actions/${id}/match-host`,
|
||||
method: 'PATCH',
|
||||
body
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyMatchEventAction: build.mutation<
|
||||
ActionSingleResponse,
|
||||
{ id: string; body: ModifyActionMatchEventRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({
|
||||
url: `actions/${id}/match-event`,
|
||||
method: 'PATCH',
|
||||
body
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyMatchProviderAction: build.mutation<
|
||||
ActionSingleResponse,
|
||||
{ id: string; body: ModifyActionMatchProviderRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({
|
||||
url: `actions/${id}/match-provider`,
|
||||
method: 'PATCH',
|
||||
body
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyTypeAndPayloadAction: build.mutation<
|
||||
ActionSingleResponse,
|
||||
{ id: string; body: ModifyActionPayloadRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({
|
||||
url: `actions/${id}/payload`,
|
||||
method: 'PATCH',
|
||||
body
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyEnabledAction: build.mutation<ActionSingleResponse, { id: string; body: ModifyActionEnabledRequest }>(
|
||||
{
|
||||
query: ({ id, body }) => ({ url: `actions/${id}/enabled`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Actions, id: arg.id }];
|
||||
}
|
||||
}
|
||||
),
|
||||
deleteAction: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `actions/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetActionsQuery,
|
||||
useGetActionByIdQuery,
|
||||
useDeleteActionMutation,
|
||||
useModifyLabelActionMutation,
|
||||
useModifyMatchEventActionMutation,
|
||||
useModifyMatchApplicationActionMutation,
|
||||
useModifyMatchHostActionMutation,
|
||||
useModifyMatchProviderActionMutation,
|
||||
useModifyTypeAndPayloadActionMutation,
|
||||
useModifyEnabledActionMutation,
|
||||
useCreateActionMutation,
|
||||
useTestActionMutation
|
||||
} = actionsApi;
|
60
server/web/src/api/eventsApi.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import EventFilterQueryParamNames from '../constants/api/eventFilterQueryParamNames';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { EventSingleResponse, EventsRequestParams, EventsResponse } from '../types/event';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
export const eventsApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getEvents: build.query<EventsResponse, EventsRequestParams>({
|
||||
query: ({ size, skip, order, orderBy }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (size) {
|
||||
params.append(EventFilterQueryParamNames.SIZE, `${size}`);
|
||||
}
|
||||
if (skip) {
|
||||
params.append(EventFilterQueryParamNames.SKIP, `${skip}`);
|
||||
}
|
||||
if (order) {
|
||||
params.append(EventFilterQueryParamNames.ORDER, `${order}`);
|
||||
}
|
||||
if (orderBy) {
|
||||
params.append(EventFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||
}
|
||||
return { url: `events?${params.toString()}` };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.Events, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.Events, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
getEventById: build.query<EventSingleResponse, { id: string }>({
|
||||
query: ({ id }) => ({ url: `events/${id}` }),
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data) {
|
||||
return [{ type: ApiTags.Events, id: result.data.id }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
deleteEvent: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `events/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Events, id: arg.id }];
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useLazyGetEventsQuery, useGetEventByIdQuery, useDeleteEventMutation } = eventsApi;
|
16
server/web/src/api/healthApi.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { HealthResponse } from '../types';
|
||||
|
||||
export const healthApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getHealth: build.query<HealthResponse, void>({
|
||||
query: () => ({ url: 'health' }),
|
||||
providesTags: [ApiTags.Health]
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetHealthQuery } = healthApi;
|
40
server/web/src/api/index.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import ApiTags from '../constants/apiTags';
|
||||
import getConfiguration from '../getConfiguration';
|
||||
import { updateAuth } from '../slices/authSlice';
|
||||
import { RootState } from '../store';
|
||||
import { BaseQueryApi, createApi, FetchArgs, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: getConfiguration().VITE_API_URL,
|
||||
prepareHeaders: (headers, { getState }) => {
|
||||
const state = getState() as RootState;
|
||||
const username = state.auth.username;
|
||||
const password = state.auth.password;
|
||||
const authHeader = window.btoa(`${username}:${password}`);
|
||||
|
||||
if (username && password && authHeader) {
|
||||
headers.set('Authorization', `Basic ${authHeader}`);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
});
|
||||
|
||||
const baseQueryWithReAuth = async (args: string | FetchArgs, api: BaseQueryApi, extraOptions: any) => {
|
||||
let result = await baseQuery(args, api, extraOptions);
|
||||
|
||||
if (result?.meta?.response?.status === 401) {
|
||||
api.dispatch(updateAuth({ username: null, password: null }));
|
||||
result = await baseQuery(args, api, extraOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const api = createApi({
|
||||
reducerPath: 'api',
|
||||
baseQuery: baseQueryWithReAuth,
|
||||
tagTypes: Object.values(ApiTags),
|
||||
endpoints: () => ({})
|
||||
});
|
||||
|
||||
export const { injectEndpoints } = api;
|
16
server/web/src/api/infoApi.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { InfoResponse } from '../types';
|
||||
|
||||
export const infoApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getInfo: build.query<InfoResponse, void>({
|
||||
query: () => ({ url: 'info' }),
|
||||
providesTags: [ApiTags.Info]
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetInfoQuery } = infoApi;
|
20
server/web/src/api/loginApi.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import { LoginRequest } from '../types';
|
||||
|
||||
export const loginApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getProbeLogin: build.mutation<void, Partial<LoginRequest>>({
|
||||
query: (body) => ({
|
||||
url: 'login', // requires an endpoint which return 204 on successful login via basic auth
|
||||
headers: {
|
||||
Authorization: `Basic ${window.btoa(body.username + ':' + body.password)}`
|
||||
}
|
||||
}),
|
||||
invalidatesTags: []
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetProbeLoginMutation } = loginApi;
|
54
server/web/src/api/secretsApi.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { CreateSecretRequest, ModifySecretValueRequest, SecretSingleResponse, SecretsResponse } from '../types';
|
||||
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
const invalidatesTags = (results?: SecretsResponse | SecretSingleResponse | void, error?: FetchBaseQueryError) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [ApiTags.Secrets] as any;
|
||||
};
|
||||
|
||||
export const secretsApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getSecrets: build.query<SecretsResponse, void>({
|
||||
query: () => {
|
||||
return { url: 'secrets' };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.Secrets, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.Secrets, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
createSecret: build.mutation<SecretSingleResponse, CreateSecretRequest>({
|
||||
query: (body) => ({ url: 'secrets', method: 'POST', body }),
|
||||
invalidatesTags
|
||||
}),
|
||||
modifyValueSecret: build.mutation<SecretSingleResponse, { id: string; body: ModifySecretValueRequest }>({
|
||||
query: ({ id, body }) => ({ url: `secrets/${id}/value`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Secrets, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
deleteSecret: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `secrets/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetSecretsQuery, useDeleteSecretMutation, useCreateSecretMutation, useModifyValueSecretMutation } =
|
||||
secretsApi;
|
84
server/web/src/api/updatesApi.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import UpdateFilterQueryParamNames from '../constants/api/updateFilterQueryParamNames';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import { ModifyUpdateStateRequest, UpdateSingleResponse, UpdatesRequestParams, UpdatesResponse } from '../types';
|
||||
import { forEach } from 'lodash';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
export const updatesApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getUpdates: build.query<UpdatesResponse, UpdatesRequestParams>({
|
||||
query: ({ ...args }) => {
|
||||
const { page, pageSize, order, orderBy, state, searchIn, searchTerm } = args;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (state) {
|
||||
forEach(state, (s) => {
|
||||
params.append(UpdateFilterQueryParamNames.STATE, s);
|
||||
});
|
||||
}
|
||||
if (searchIn) {
|
||||
params.append(UpdateFilterQueryParamNames.SEARCH_IN, `${searchIn}`);
|
||||
}
|
||||
if (searchTerm) {
|
||||
params.append(UpdateFilterQueryParamNames.SEARCH_TERM, `${searchTerm}`);
|
||||
}
|
||||
if (page) {
|
||||
params.append(UpdateFilterQueryParamNames.PAGE, `${page}`);
|
||||
}
|
||||
if (pageSize) {
|
||||
params.append(UpdateFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||
}
|
||||
if (order) {
|
||||
params.append(UpdateFilterQueryParamNames.ORDER, `${order}`);
|
||||
}
|
||||
if (orderBy) {
|
||||
params.append(UpdateFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||
}
|
||||
return { url: `updates?${params.toString()}` };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.Updates, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.Updates, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
getUpdateById: build.query<UpdateSingleResponse, { id: string }>({
|
||||
query: ({ id }) => ({ url: `updates/${id}` }),
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data) {
|
||||
return [{ type: ApiTags.Updates, id: result.data.id }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
modifyUpdateState: build.mutation<UpdateSingleResponse, { id: string; body: ModifyUpdateStateRequest }>({
|
||||
query: ({ id, body }) => ({ url: `updates/${id}/state`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Updates, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
deleteUpdate: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `updates/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (error) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Updates }];
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const { useGetUpdatesQuery, useGetUpdateByIdQuery, useModifyUpdateStateMutation, useDeleteUpdateMutation } =
|
||||
updatesApi;
|
92
server/web/src/api/webhooksApi.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { injectEndpoints } from './index';
|
||||
import WebhookFilterQueryParamNames from '../constants/api/webhookFilterQueryParamNames';
|
||||
import ApiTags from '../constants/apiTags';
|
||||
import {
|
||||
CreateWebhookRequest,
|
||||
ModifyWebhookIgnoreHostRequest,
|
||||
ModifyWebhookLabelRequest,
|
||||
WebhookSingleResponse,
|
||||
WebhooksRequestParams,
|
||||
WebhooksResponse
|
||||
} from '../types';
|
||||
import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||
|
||||
const TAG_LIST_ID = 'LIST';
|
||||
|
||||
const invalidatesTags = (results?: WebhooksResponse | WebhookSingleResponse | void, error?: FetchBaseQueryError) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [ApiTags.Webhooks] as any;
|
||||
};
|
||||
|
||||
export const webhooksApi = injectEndpoints({
|
||||
endpoints: (build) => {
|
||||
return {
|
||||
getWebhooks: build.query<WebhooksResponse, WebhooksRequestParams>({
|
||||
query: ({ page, pageSize, order, orderBy }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (page) {
|
||||
params.append(WebhookFilterQueryParamNames.PAGE, `${page}`);
|
||||
}
|
||||
if (pageSize) {
|
||||
params.append(WebhookFilterQueryParamNames.PAGE_SIZE, `${pageSize}`);
|
||||
}
|
||||
if (order) {
|
||||
params.append(WebhookFilterQueryParamNames.ORDER, `${order}`);
|
||||
}
|
||||
if (orderBy) {
|
||||
params.append(WebhookFilterQueryParamNames.ORDER_BY, `${orderBy}`);
|
||||
}
|
||||
return { url: `webhooks?${params.toString()}` };
|
||||
},
|
||||
providesTags: (result, error) => {
|
||||
if (!error && result?.data.content) {
|
||||
return [
|
||||
{ type: ApiTags.Webhooks, id: TAG_LIST_ID },
|
||||
...result.data.content.map(({ id }) => ({ type: ApiTags.Webhooks, id }))
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
createWebhook: build.mutation<WebhookSingleResponse, CreateWebhookRequest>({
|
||||
query: (body) => ({ url: 'webhooks', method: 'POST', body }),
|
||||
invalidatesTags
|
||||
}),
|
||||
deleteWebhook: build.mutation<void, { id: string }>({
|
||||
query: ({ id }) => ({ url: `webhooks/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags
|
||||
}),
|
||||
modifyLabelWebhook: build.mutation<WebhookSingleResponse, { id: string; body: ModifyWebhookLabelRequest }>({
|
||||
query: ({ id, body }) => ({ url: `webhooks/${id}/label`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Webhooks, id: arg.id }];
|
||||
}
|
||||
}),
|
||||
modifyIgnoreHostWebhook: build.mutation<
|
||||
WebhookSingleResponse,
|
||||
{ id: string; body: ModifyWebhookIgnoreHostRequest }
|
||||
>({
|
||||
query: ({ id, body }) => ({ url: `webhooks/${id}/ignore-host`, method: 'PATCH', body }),
|
||||
invalidatesTags: (result, error, arg) => {
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return [{ type: ApiTags.Webhooks, id: arg.id }];
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetWebhooksQuery,
|
||||
useDeleteWebhookMutation,
|
||||
useCreateWebhookMutation,
|
||||
useModifyLabelWebhookMutation,
|
||||
useModifyIgnoreHostWebhookMutation
|
||||
} = webhooksApi;
|
|
@ -0,0 +1,8 @@
|
|||
enum ActionFilterQueryParamNames {
|
||||
PAGE = 'page',
|
||||
PAGE_SIZE = 'pageSize',
|
||||
ORDER = 'order',
|
||||
ORDER_BY = 'orderBy'
|
||||
}
|
||||
|
||||
export default ActionFilterQueryParamNames;
|
|
@ -0,0 +1,8 @@
|
|||
enum ActionInvocationFilterQueryParamNames {
|
||||
PAGE = 'page',
|
||||
PAGE_SIZE = 'pageSize',
|
||||
ORDER = 'order',
|
||||
ORDER_BY = 'orderBy'
|
||||
}
|
||||
|
||||
export default ActionInvocationFilterQueryParamNames;
|
6
server/web/src/constants/api/actionInvocationOrder.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
enum ActionInvocationOrder {
|
||||
DESC = 'desc',
|
||||
ASC = 'asc'
|
||||
}
|
||||
|
||||
export default ActionInvocationOrder;
|