mirror of
https://github.com/foomo/contentserver.git
synced 2025-10-16 12:25:44 +00:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e5d09f50 | ||
|
|
d5498dde73 | ||
|
|
fe1d9de6f1 | ||
|
|
74d2ff75f5 | ||
|
|
eefd5bbc57 | ||
|
|
29ca34e0d5 | ||
|
|
1fcc058fab | ||
|
|
b500a35214 | ||
|
|
c5d4b210f9 | ||
|
|
d84f3f0941 | ||
|
|
7a588bd108 | ||
|
|
6c104da662 | ||
|
|
d058ce2b41 | ||
|
|
1ac5dab2aa | ||
|
|
a53de5827c | ||
|
|
b4b9de6df3 | ||
|
|
368dcf4f3f | ||
|
|
ff7d3da5d1 | ||
|
|
c95742339f | ||
|
|
f0e84fc185 | ||
|
|
71155b45e6 | ||
|
|
2ea30be4b5 | ||
|
|
56aa5220ac | ||
|
|
dd8e6a63e7 | ||
|
|
2bb303501e | ||
|
|
1e7676bf73 | ||
|
|
680e0cdd33 | ||
|
|
924bc01c07 | ||
|
|
73203b24c0 | ||
|
|
a2ae079ae9 | ||
|
|
327ca96a76 | ||
|
|
5ea6ad27d2 | ||
|
|
ec843ef283 | ||
|
|
8fdeef555b | ||
|
|
15ab819b6a | ||
|
|
c214ba4a4d | ||
|
|
0e61ff81ed | ||
|
|
8081e3c762 | ||
|
|
1d0fa88b50 | ||
|
|
a57fafaf0f | ||
|
|
620d759ce8 | ||
|
|
34d35ad779 | ||
|
|
318173f848 | ||
|
|
ec6ff54135 | ||
|
|
e50df5eaf9 | ||
|
|
ea920005e9 | ||
|
|
f8617b3da2 | ||
|
|
71dfd39ff5 | ||
|
|
1617ebbd84 | ||
|
|
554733f1c7 | ||
|
|
9af5f53ce7 | ||
|
|
d46ba48b4b | ||
|
|
3717872187 | ||
|
|
1c472c13bf | ||
|
|
b4f8b95a4b | ||
|
|
ae481c5278 | ||
|
|
5e7798a53f | ||
|
|
d9a8248c0a | ||
|
|
abb6f692c7 | ||
|
|
52c1a67b7a | ||
|
|
3f8561359c | ||
|
|
af95153899 | ||
|
|
bb04560323 | ||
|
|
84135f3d8f | ||
|
|
555fc8a5c2 | ||
|
|
f1dae6a745 | ||
|
|
da832d3f52 | ||
|
|
39cd0c422a | ||
|
|
0c8267134e | ||
|
|
3a4a69e2cf | ||
|
|
cac32f2d52 | ||
|
|
f3366f5211 | ||
|
|
2fa98dad5e | ||
|
|
be835e3ba6 | ||
|
|
efdfd11760 | ||
|
|
0e44ca809d | ||
|
|
3440cbdc0e | ||
|
|
c2837eec07 | ||
|
|
006bd6ea0a | ||
|
|
ac85c31b77 | ||
|
|
93fcca1a5f | ||
|
|
ae29f1060c | ||
|
|
5788fd203d | ||
|
|
c9bf5666e5 | ||
|
|
ffa04ace36 | ||
|
|
24c65ba8df | ||
|
|
ddb4adf571 | ||
|
|
0ab7935a05 | ||
|
|
f75f3a09ae | ||
|
|
422fcf95b6 | ||
|
|
7280bc5e03 | ||
|
|
2b9d7ea9e7 | ||
|
|
934b1dc9c4 | ||
|
|
620291db4a | ||
|
|
130ab553e3 | ||
|
|
5ce4cccbea | ||
|
|
f7f3b8096e | ||
|
|
3f02444b97 | ||
|
|
7e59a0dc71 | ||
|
|
864df48b90 | ||
|
|
b41951f51e | ||
|
|
16fd473458 | ||
|
|
6bf4e20444 | ||
|
|
fd0c81bc23 | ||
|
|
25afa0523d | ||
|
|
aed699f987 | ||
|
|
1911f68de6 | ||
|
|
d15c524be8 | ||
|
|
0919233472 | ||
|
|
473da013c9 | ||
|
|
2ab4c0364c | ||
|
|
fd4f87da95 | ||
|
|
0661a69601 | ||
|
|
e6e95db586 | ||
|
|
a2b0eabb41 | ||
|
|
f5d1117c67 | ||
|
|
6381c7c0c2 | ||
|
|
581e68599c | ||
|
|
1c814a450c | ||
|
|
2cf28f7217 | ||
|
|
8197ec0931 | ||
|
|
8d85fc5f81 | ||
|
|
0aed28b524 | ||
|
|
fcc36028c1 | ||
|
|
1d3405cbf7 | ||
|
|
9e8a0cb6d3 | ||
|
|
735a0ab3f8 | ||
|
|
3b4a55f18e | ||
|
|
a5ff003d8f | ||
|
|
871c844f7b | ||
|
|
8224e92d4d | ||
|
|
1b7aa6475e | ||
|
|
aa0f6695d7 | ||
|
|
4e6eecc673 | ||
|
|
69dec41605 | ||
|
|
5cff674940 | ||
|
|
33364e3af8 | ||
|
|
647853292b | ||
|
|
79d828bb23 | ||
|
|
7c29ec73e4 | ||
|
|
71403194e2 | ||
|
|
e9245a200c | ||
|
|
d9f6cc60c4 | ||
|
|
e874cc2b16 | ||
|
|
2decb53ec1 | ||
|
|
2faa088178 | ||
|
|
e2a51bb5a5 | ||
|
|
0056f53b97 | ||
|
|
28292faea9 | ||
|
|
a4097c05f4 | ||
|
|
97633dc9d9 | ||
|
|
eca5e3b4f0 | ||
|
|
f7aea048d3 | ||
|
|
1449d6902c | ||
|
|
18897d2e32 | ||
|
|
0827eb9b4a | ||
|
|
63640b24b2 | ||
|
|
3985784579 | ||
|
|
55fd63b82d | ||
|
|
295cdf66fc | ||
|
|
03d5b36706 | ||
|
|
65b6b2341a | ||
|
|
0735b5ad18 | ||
|
|
f20402ef5f | ||
|
|
dc047baf32 | ||
|
|
f0df9a6322 | ||
|
|
5e44495adc | ||
|
|
e64b07f6c6 | ||
|
|
b713a41cf6 | ||
|
|
5ee042bcd4 | ||
|
|
54ee995295 | ||
|
|
82733d4b25 | ||
|
|
dd902fe717 | ||
|
|
d2e0af8aca | ||
|
|
a267dbe1ec | ||
|
|
bdb694b0ff | ||
|
|
9d94c09735 | ||
|
|
7f9b32162e | ||
|
|
284ee99690 | ||
|
|
73b9b71dd3 | ||
|
|
8e574d0675 | ||
|
|
757c310f8d | ||
|
|
42adb6a25a | ||
|
|
5beb586365 | ||
|
|
79244f8ab3 | ||
|
|
b7f10ed673 | ||
|
|
ff28d6670f | ||
|
|
d0e9a2966a | ||
|
|
6b891e7f0f | ||
|
|
3eac4bd1b7 | ||
|
|
8f7c23ff4a | ||
|
|
3479cd7eaf | ||
|
|
a120c09c82 | ||
|
|
f96a5669a8 | ||
|
|
37147120e2 | ||
|
|
2c5492ebe4 | ||
|
|
bbded7c9db | ||
|
|
6eb33c6978 | ||
|
|
ef3ef2bc50 | ||
|
|
1a64987d8d | ||
|
|
7decb6b56f |
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yaml,yml,md,mdx}]
|
||||
indent_style = space
|
||||
133
.github/CODE_OF_CONDUCT.md
vendored
Normal file
133
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
info@bestbytes.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
21
.github/CONTRIBUTING.md
vendored
Normal file
21
.github/CONTRIBUTING.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Contributing
|
||||
|
||||
If you want to submit a pull request to fix a bug or enhance an existing
|
||||
feature, please first open an issue and link to that issue when you
|
||||
submit your pull request.
|
||||
|
||||
If you have any questions about a possible submission, feel free to open
|
||||
an issue too.
|
||||
|
||||
### Pull request process
|
||||
|
||||
1. Fork this repository
|
||||
2. Create a branch in your fork to implement the changes. We recommend using
|
||||
the issue number as part of your branch name, e.g. `1234-fixes`
|
||||
3. Ensure that any documentation is updated with the changes that are required
|
||||
by your fix.
|
||||
4. Ensure that any samples are updated if the base image has been changed.
|
||||
5. Submit the pull request. *Do not leave the pull request blank*. Explain exactly
|
||||
what your changes are meant to do and provide simple steps on how to validate
|
||||
your changes. Ensure that you reference the issue you created as well.
|
||||
The pull request will be review before it is merged.
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a bug you encountered
|
||||
labels: bug
|
||||
---
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
- Affected Version:
|
||||
- OS (e.g: `cat /etc/os-release`):
|
||||
- Others:
|
||||
8
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Enhancement Request
|
||||
about: Suggest an enhancement
|
||||
labels: enhancement
|
||||
---
|
||||
**What would you like to be added**:
|
||||
|
||||
**Why is this needed**:
|
||||
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
### Type of Change
|
||||
- [ ] New feature
|
||||
- [ ] Bug fix
|
||||
- [ ] Documentation update
|
||||
- [ ] Refactoring
|
||||
- [ ] Hotfix
|
||||
- [ ] Security patch
|
||||
|
||||
### Description
|
||||
_[Provide a detailed explanation of the changes you have made. Include the reasons behind these changes and any relevant context. Link any related issues.]_
|
||||
|
||||
### Related Issues
|
||||
_[If this pull request addresses an issue, please link to it here (e.g., Fixes #123).]_
|
||||
|
||||
### Checklist
|
||||
- [ ] My code adheres to the coding and style guidelines of the project.
|
||||
- [ ] I have performed a self-review of my own code.
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas.
|
||||
- [ ] I have made corresponding changes to the documentation.
|
||||
45
.github/SECURITY.md
vendored
Normal file
45
.github/SECURITY.md
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# Security Guidelines
|
||||
|
||||
## How security is managed on this project
|
||||
|
||||
The foomo team and community take security seriously and wants to ensure that
|
||||
we maintain a secure environment and provide secure solutions for the open
|
||||
source community. To help us achieve these goals, please note the
|
||||
following before using this software:
|
||||
|
||||
- Review the software license to understand the contributor's obligations in
|
||||
terms of warranties and suitability for purpose
|
||||
- For any questions or concerns about security, you can
|
||||
[create an issue][new-issue] or [report a vulnerability][new-sec-issue]
|
||||
- We request that you work with our security team and opt for
|
||||
responsible disclosure using the guidelines below
|
||||
- All security related issues and pull requests you make should be tagged with
|
||||
"security" for easy identification
|
||||
- Please monitor this repository and update your environment in a timely manner
|
||||
as we release patches and updates
|
||||
|
||||
## Responsibly Disclosing Security Bugs
|
||||
|
||||
If you find a security bug in this repository, please work with contributors
|
||||
following responsible disclosure principles and these guidelines:
|
||||
|
||||
- Do not submit a normal issue or pull request in our public repository, instead
|
||||
[report it directly][new-sec-issue].
|
||||
- We will review your submission and may follow up for additional details
|
||||
- If you have a patch, we will review it and approve it privately; once approved
|
||||
for release you can submit it as a pull request publicly in the repository (we
|
||||
give credit where credit is due)
|
||||
- We will keep you informed during our investigation, feel free to check in for
|
||||
a status update
|
||||
- We will release the fix and publicly disclose the issue as soon as possible,
|
||||
but want to ensure we due properly due diligence before releasing
|
||||
- Please do not publicly blog or post about the security issue until after we
|
||||
have updated the public repo so that other downstream users have an opportunity
|
||||
to patch
|
||||
|
||||
## Contact / Misc
|
||||
|
||||
If you have any questions, please reach out directly by [creating an issue][new-issue].
|
||||
|
||||
[new-issue]: https://github.com/foomo/contentserver/issues/new/choose
|
||||
[new-sec-issue]: https://github.com/foomo/contentserver/security/advisories/new
|
||||
BIN
.github/assets/contentserver.png
vendored
Normal file
BIN
.github/assets/contentserver.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
52
.github/dependabot.yml
vendored
Normal file
52
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
open-pull-requests-limit: 1
|
||||
directory: '/'
|
||||
schedule:
|
||||
day: 'sunday'
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ['*']
|
||||
|
||||
- package-ecosystem: 'gomod'
|
||||
open-pull-requests-limit: 1
|
||||
directory: '/'
|
||||
schedule:
|
||||
day: 'sunday'
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
gomod-security:
|
||||
applies-to: security-updates
|
||||
update-types: ['minor', 'patch']
|
||||
patterns: ['*']
|
||||
gomod-update:
|
||||
applies-to: version-updates
|
||||
update-types: ['minor', 'patch']
|
||||
patterns: ['*']
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
- package-ecosystem: docker
|
||||
open-pull-requests-limit: 1
|
||||
directory: '/build'
|
||||
schedule:
|
||||
day: 'sunday'
|
||||
interval: 'weekly'
|
||||
groups:
|
||||
docker-security:
|
||||
applies-to: security-updates
|
||||
update-types: ['minor', 'patch']
|
||||
patterns: ['*']
|
||||
docker-update:
|
||||
applies-to: version-updates
|
||||
update-types: ['minor', 'patch']
|
||||
patterns: ['*']
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
37
.github/workflows/pr.yml
vendored
Normal file
37
.github/workflows/pr.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- uses: gotesttools/gotestfmt-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
- uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
file: coverage.out
|
||||
43
.github/workflows/tag.yml
vendored
Normal file
43
.github/workflows/tag.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Release Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- id: app_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Login to docker.io
|
||||
run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --timeout=90m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@ -1,5 +1,29 @@
|
||||
.*
|
||||
*~
|
||||
*.zip
|
||||
*.tar
|
||||
*.out
|
||||
*.log
|
||||
/bin/
|
||||
/pkg/tmp/
|
||||
!.git*
|
||||
/dist/
|
||||
/tmp/
|
||||
|
||||
## Git
|
||||
!.gitkeep
|
||||
!.gitignore
|
||||
|
||||
## GitHub
|
||||
!.github/
|
||||
|
||||
## Editorconfig
|
||||
!.editorconfig
|
||||
|
||||
## Husky
|
||||
!.husky/
|
||||
!.husky.yaml
|
||||
|
||||
## Ownbrew
|
||||
!.ownbrew.yaml
|
||||
|
||||
## Golang
|
||||
!.golangci.yml
|
||||
!.goreleaser.yml
|
||||
|
||||
152
.golangci.yml
Normal file
152
.golangci.yml
Normal file
@ -0,0 +1,152 @@
|
||||
version: "2"
|
||||
run:
|
||||
go: 1.24.3
|
||||
build-tags: [safe]
|
||||
modules-download-mode: readonly
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
## Default linters
|
||||
- errcheck # errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
|
||||
- govet # (vet, vetshadow) Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes. [fast: false, auto-fix: false]
|
||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||
- staticcheck # (megacheck) It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
|
||||
- unused # (megacheck) Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||
|
||||
## Recommended linters
|
||||
- asasalint # check for pass []any as any in variadic func(...any) [fast: false, auto-fix: false]
|
||||
- asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name [fast: true, auto-fix: false]
|
||||
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
|
||||
- canonicalheader # checks whether net/http.Header uses canonical header [fast: false, auto-fix: false]
|
||||
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: false, auto-fix: false]
|
||||
- contextcheck # check whether the function uses a non-inherited context [fast: false, auto-fix: false]
|
||||
- copyloopvar # (go >= 1.22) copyloopvar is a linter detects places where loop variables are copied [fast: true, auto-fix: false]
|
||||
- decorder # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false]
|
||||
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
||||
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
|
||||
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
|
||||
#- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
|
||||
#- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
|
||||
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix]
|
||||
- fatcontext # detects nested contexts in loops and function literals [fast: false, auto-fix: false]
|
||||
#- forbidigo # Forbids identifiers [fast: false, auto-fix: false]
|
||||
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
|
||||
- gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false]
|
||||
- gochecksumtype # Run exhaustiveness checks on Go "sum types" [fast: false, auto-fix: false]
|
||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||
- gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: true]
|
||||
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: true]
|
||||
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
|
||||
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
|
||||
- goprintffuncname # Checks that printf-like functions are named with `f` at the end. [fast: true, auto-fix: false]
|
||||
- gosec # (gas) Inspects source code for security problems [fast: false, auto-fix: false]
|
||||
- gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase [fast: false, auto-fix: false]
|
||||
- grouper # Analyze expression groups. [fast: true, auto-fix: false]
|
||||
- iface # Detect the incorrect use of interfaces, helping developers avoid interface pollution. [fast: false, auto-fix: false]
|
||||
- importas # Enforces consistent import aliases [fast: false, auto-fix: false]
|
||||
- inamedparam # reports interfaces with unnamed method parameters [fast: true, auto-fix: false]
|
||||
#- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false]
|
||||
- loggercheck # (logrlint) Checks key value pairs for common logger libraries (kitlog,klog,logr,zap). [fast: false, auto-fix: false]
|
||||
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
|
||||
- mirror # reports wrong mirror patterns of bytes/strings usage [fast: false, auto-fix: true]
|
||||
- misspell # Finds commonly misspelled English words [fast: true, auto-fix: true]
|
||||
- musttag # enforce field tags in (un)marshaled structs [fast: false, auto-fix: false]
|
||||
- nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). [fast: true, auto-fix: false]
|
||||
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
|
||||
- nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error.
|
||||
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
|
||||
- noctx # Finds sending http request without context.Context [fast: false, auto-fix: false]
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: true]
|
||||
#- nonamedreturns # Reports all named returns [fast: false, auto-fix: false]
|
||||
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. [fast: true, auto-fix: false]
|
||||
#- paralleltest # Detects missing usage of t.Parallel() method in your Go test [fast: false, auto-fix: false]
|
||||
- predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
|
||||
- promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false]
|
||||
- reassign # Checks that package variables are not reassigned [fast: false, auto-fix: false]
|
||||
- recvcheck # checks for receiver type consistency [fast: false, auto-fix: false]
|
||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
|
||||
- rowserrcheck # checks whether Rows.Err of rows is checked successfully [fast: false, auto-fix: false]
|
||||
- spancheck # Checks for mistakes with OpenTelemetry/Census spans. [fast: false, auto-fix: false]
|
||||
- sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. [fast: false, auto-fix: false]
|
||||
- testableexamples # linter checks if examples are testable (have an expected output) [fast: true, auto-fix: false]
|
||||
- testifylint # Checks usage of github.com/stretchr/testify. [fast: false, auto-fix: false]
|
||||
#- testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
|
||||
- thelper # thelper detects tests helpers which is not start with t.Helper() method. [fast: false, auto-fix: false]
|
||||
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. [fast: false, auto-fix: false]
|
||||
- unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false]
|
||||
- usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false]
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package. [auto-fix]
|
||||
- wastedassign # Finds wasted assignment statements [fast: false, auto-fix: false]
|
||||
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. [fast: true, auto-fix: true]
|
||||
|
||||
## Discouraged linters
|
||||
#- cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false]
|
||||
#- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||
#- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||
#- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
||||
#- dupword # checks for duplicate words in the source code [fast: true, auto-fix: false]
|
||||
#- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||
#- err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false]
|
||||
#- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false]
|
||||
#- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||
#- ginkgolinter # enforces standards of using ginkgo and gomega [fast: false, auto-fix: false]
|
||||
#- gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false]
|
||||
#- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
||||
#- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
|
||||
#- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
|
||||
#- godot # Check if comments end in a period [fast: true, auto-fix: true]
|
||||
#- godox # Tool for detection of comment keywords [fast: true, auto-fix: false]
|
||||
#- interfacebloat # A linter that checks the number of methods inside an interface. [fast: true, auto-fix: false]
|
||||
#- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false]
|
||||
#- ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false]
|
||||
#- lll # Reports long lines [fast: true, auto-fix: false]
|
||||
#- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false]
|
||||
#- nestif # Reports deeply nested if statements [fast: true, auto-fix: false]
|
||||
#- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
|
||||
#- mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
|
||||
#- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. [fast: false, auto-fix: false]
|
||||
#- prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false]
|
||||
#- protogetter # Reports direct reads from proto message fields when getters should be used [fast: false, auto-fix: true]
|
||||
#- sloglint # ensure consistent code style when using log/slog [fast: false, auto-fix: false]
|
||||
#- tagalign # check that struct tags are well aligned [fast: true, auto-fix: true]
|
||||
#- tagliatelle # Checks the struct tags. [fast: true, auto-fix: false]
|
||||
#- unparam # Reports unused function parameters [fast: false, auto-fix: false]
|
||||
#- varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false]
|
||||
#- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||
#- wsl # add or remove empty lines [fast: true, auto-fix: false]
|
||||
#- zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg` [fast: false, auto-fix: false]
|
||||
settings:
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- commentFormatting
|
||||
revive:
|
||||
rules:
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- bin
|
||||
- tmp
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- bin
|
||||
- tmp
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
119
.goreleaser.yml
Normal file
119
.goreleaser.yml
Normal file
@ -0,0 +1,119 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
|
||||
project_name: contentserver
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: foomo
|
||||
name: contentserver
|
||||
prerelease: auto
|
||||
|
||||
builds:
|
||||
- binary: contentserver
|
||||
main: ./main.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goarm:
|
||||
- '7'
|
||||
flags:
|
||||
- -trimpath
|
||||
- -tags=safe
|
||||
ldflags:
|
||||
- -s -w -X github.com/foomo/contentserver/cmd.version={{.Version}}
|
||||
|
||||
archives:
|
||||
- formats: [ tar.gz ]
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [ zip ]
|
||||
|
||||
changelog:
|
||||
use: github-native
|
||||
|
||||
brews:
|
||||
- repository:
|
||||
owner: foomo
|
||||
name: homebrew-tap
|
||||
caveats: "contentserver --help"
|
||||
homepage: "https://github.com/foomo/contentserver"
|
||||
description: "Serves content tree structures very quickly"
|
||||
test: |
|
||||
system "#{bin}/contentserver version"
|
||||
|
||||
dockers:
|
||||
- use: buildx
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
dockerfile: build/buildx.Dockerfile
|
||||
image_templates:
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:latest-amd64{{ end }}'
|
||||
- 'foomo/contentserver:{{ .Version }}-amd64'
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}-amd64{{ end }}'
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}.{{ .Minor }}-amd64{{ end }}'
|
||||
build_flag_templates:
|
||||
- '--pull'
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
- '--label=org.opencontainers.image.title={{.ProjectName}}'
|
||||
- '--label=org.opencontainers.image.description=Serves content tree structures very quickly'
|
||||
- '--label=org.opencontainers.image.source={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.url={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.documentation={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.created={{.Date}}'
|
||||
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
|
||||
- '--label=org.opencontainers.image.version={{.Version}}'
|
||||
- '--platform=linux/amd64'
|
||||
|
||||
- use: buildx
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
dockerfile: build/buildx.Dockerfile
|
||||
image_templates:
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:latest-arm64{{ end }}'
|
||||
- 'foomo/contentserver:{{ .Version }}-arm64'
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}-arm64{{ end }}'
|
||||
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}.{{ .Minor }}-arm64{{ end }}'
|
||||
build_flag_templates:
|
||||
- '--pull'
|
||||
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
- '--label=org.opencontainers.image.title={{.ProjectName}}'
|
||||
- '--label=org.opencontainers.image.description=Serves content tree structures very quickly'
|
||||
- '--label=org.opencontainers.image.source={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.url={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.documentation={{.GitURL}}'
|
||||
- '--label=org.opencontainers.image.created={{.Date}}'
|
||||
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
|
||||
- '--label=org.opencontainers.image.version={{.Version}}'
|
||||
- '--platform=linux/arm64'
|
||||
|
||||
docker_manifests:
|
||||
# basic
|
||||
- name_template: 'foomo/contentserver:latest'
|
||||
image_templates:
|
||||
- 'foomo/contentserver:latest-amd64'
|
||||
- 'foomo/contentserver:latest-arm64'
|
||||
skip_push: auto
|
||||
|
||||
- name_template: 'foomo/contentserver:{{ .Version }}'
|
||||
image_templates:
|
||||
- 'foomo/contentserver:{{ .Version }}-amd64'
|
||||
- 'foomo/contentserver:{{ .Version }}-arm64'
|
||||
|
||||
- name_template: 'foomo/contentserver:{{ .Major }}'
|
||||
image_templates:
|
||||
- 'foomo/contentserver:{{ .Major }}-amd64'
|
||||
- 'foomo/contentserver:{{ .Major }}-arm64'
|
||||
skip_push: auto
|
||||
|
||||
- name_template: 'foomo/contentserver:{{ .Major }}.{{ .Minor }}'
|
||||
image_templates:
|
||||
- 'foomo/contentserver:{{ .Major }}.{{ .Minor }}-amd64'
|
||||
- 'foomo/contentserver:{{ .Major }}.{{ .Minor }}-arm64'
|
||||
skip_push: auto
|
||||
15
.husky.yaml
Normal file
15
.husky.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
hooks:
|
||||
pre-commit:
|
||||
- golangci-lint run --fast-only
|
||||
- husky lint-staged
|
||||
commit-msg:
|
||||
# only execute if not in a merge
|
||||
- if [[ -z $(git rev-parse -q --verify MERGE_HEAD) ]]; then husky lint-commit; fi
|
||||
|
||||
lint-staged:
|
||||
'*.go':
|
||||
- golangci-lint fmt
|
||||
|
||||
lint-commit:
|
||||
types: '^(feat|fix|build|chore|docs|perf|refactor|revert|style|test|wip)$'
|
||||
header: '^(?P<type>\w+)(\((?P<scope>[\w/.-]+)\))?(?P<breaking>!)?:( +)?(?P<header>.+)'
|
||||
3
.husky/applypatch-msg
Executable file
3
.husky/applypatch-msg
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/commit-msg
Executable file
3
.husky/commit-msg
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/fsmonitor-watchman
Executable file
3
.husky/fsmonitor-watchman
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/post-update
Executable file
3
.husky/post-update
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-applypatch
Executable file
3
.husky/pre-applypatch
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-commit
Executable file
3
.husky/pre-commit
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-merge-commit
Executable file
3
.husky/pre-merge-commit
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-push
Executable file
3
.husky/pre-push
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-rebase
Executable file
3
.husky/pre-rebase
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/pre-receive
Executable file
3
.husky/pre-receive
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/prepare-commit-msg
Executable file
3
.husky/prepare-commit-msg
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/push-to-checkout
Executable file
3
.husky/push-to-checkout
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/sendemail-validate
Executable file
3
.husky/sendemail-validate
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
3
.husky/update
Executable file
3
.husky/update
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
husky hook $(basename "$0") $*
|
||||
@ -1,6 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
21
Dockerfile
21
Dockerfile
@ -1,21 +0,0 @@
|
||||
FROM scratch
|
||||
|
||||
COPY bin/contentserver-linux-amd64 /usr/sbin/contentserver
|
||||
|
||||
# install ca root certificates
|
||||
# https://curl.haxx.se/docs/caextract.html
|
||||
# http://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
|
||||
# does not work on docker for mac :(
|
||||
# ADD https://curl.haxx.se/ca/cacert.pem /etc/ssl/certs/ca-certificates.crt
|
||||
ADD .cacert.pem /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
ENV CONTENT_SERVER_LOG_LEVEL=error
|
||||
ENV CONTENT_SERVER_ADDR=0.0.0.0:80
|
||||
ENV CONTENT_SERVER_VAR_DIR=/var/lib/contentserver
|
||||
|
||||
VOLUME $CONTENT_SERVER_VAR_DIR
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/usr/sbin/contentserver"]
|
||||
|
||||
CMD ["-address=$CONTENT_SERVER_ADDR", "-log-level=$CONTENT_SERVER_LOG_LEVEL", "-var-dir=$CONTENT_SERVER_VAR_DIR"]
|
||||
124
Makefile
124
Makefile
@ -1,23 +1,109 @@
|
||||
SHELL := /bin/bash
|
||||
.DEFAULT_GOAL:=help
|
||||
-include .makerc
|
||||
|
||||
TAG=`git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || git rev-parse --abbrev-ref HEAD`
|
||||
# --- Targets -----------------------------------------------------------------
|
||||
|
||||
all: build test
|
||||
clean:
|
||||
rm -fv bin/contentserve*
|
||||
build: clean
|
||||
go build -o bin/contentserver
|
||||
build-arch: clean
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/contentserver-linux-amd64
|
||||
GOOS=darwin GOARCH=amd64 go build -o bin/contentserver-darwin-amd64
|
||||
build-docker: clean build-arch
|
||||
curl https://curl.haxx.se/ca/cacert.pem > .cacert.pem
|
||||
docker build -q . > .image_id
|
||||
docker tag `cat .image_id` docker-registry.bestbytes.net/contentserver:$(TAG)
|
||||
echo "# tagged container `cat .image_id` as docker-registry.bestbytes.net/contentserver:$(TAG)"
|
||||
rm -vf .image_id .cacert.pem
|
||||
# This allows us to accept extra arguments
|
||||
%: .husky
|
||||
@:
|
||||
|
||||
package: build
|
||||
pkg/build.sh
|
||||
.PHONY: .husky
|
||||
# Configure git hooks for husky
|
||||
.husky:
|
||||
@if ! command -v husky &> /dev/null; then \
|
||||
echo "ERROR: missing executeable 'husky', please run:"; \
|
||||
echo "\n$ go install github.com/go-courier/husky/cmd/husky@latest\n"; \
|
||||
fi
|
||||
@git config core.hooksPath .husky
|
||||
|
||||
## === Tasks ===
|
||||
|
||||
.PHONY: doc
|
||||
## Open go docs
|
||||
doc:
|
||||
@open "http://localhost:6060/pkg/github.com/foomo/contentserver/"
|
||||
@godoc -http=localhost:6060 -play
|
||||
|
||||
.PHONY: test
|
||||
## Run tests
|
||||
test:
|
||||
go test ./...
|
||||
@GO_TEST_TAGS=-skip go test -v -tags=safe -coverprofile=coverage.out -race -count=1 ./...
|
||||
#@GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -race -json ./... | gotestfmt
|
||||
|
||||
.PHONY: test.update
|
||||
## Run tests and update snapshots
|
||||
test.update:
|
||||
@GO_TEST_TAGS=-skip go test -update -v -tags=safe -coverprofile=coverage.out -race ./...
|
||||
#@GO_TEST_TAGS=-skip go test -update -tags=safe -coverprofile=coverage.out -race -json ./... | gotestfmt
|
||||
|
||||
.PHONY: lint
|
||||
## Run linter
|
||||
lint:
|
||||
@golangci-lint run
|
||||
|
||||
.PHONY: lint.fix
|
||||
## Fix lint violations
|
||||
lint.fix:
|
||||
@golangci-lint run --fix
|
||||
|
||||
.PHONY: tidy
|
||||
## Run go mod tidy
|
||||
tidy:
|
||||
@go mod tidy
|
||||
|
||||
.PHONY: outdated
|
||||
## Show outdated direct dependencies
|
||||
outdated:
|
||||
@go list -u -m -json all | go-mod-outdated -update -direct
|
||||
|
||||
.PHONY: install
|
||||
## Install binary
|
||||
install:
|
||||
@go build -tags=safe -o ${GOPATH}/bin/contentserver main.go
|
||||
|
||||
.PHONY: build
|
||||
## Build binary
|
||||
build:
|
||||
@mkdir -p bin
|
||||
@go build -tags=safe -o bin/contentserver main.go
|
||||
|
||||
.PHONY: release.snapshot
|
||||
## Create a goreleaser snapshot release
|
||||
release.snapshot:
|
||||
@rm -rf ./dist
|
||||
@goreleaser release --snapshot
|
||||
|
||||
## === Utils ===
|
||||
|
||||
.PHONY: help
|
||||
## Show help text
|
||||
help:
|
||||
@awk '{ \
|
||||
if ($$0 ~ /^.PHONY: [a-zA-Z\-\_0-9]+$$/) { \
|
||||
helpCommand = substr($$0, index($$0, ":") + 2); \
|
||||
if (helpMessage) { \
|
||||
printf "\033[36m%-23s\033[0m %s\n", \
|
||||
helpCommand, helpMessage; \
|
||||
helpMessage = ""; \
|
||||
} \
|
||||
} else if ($$0 ~ /^[a-zA-Z\-\_0-9.]+:/) { \
|
||||
helpCommand = substr($$0, 0, index($$0, ":")); \
|
||||
if (helpMessage) { \
|
||||
printf "\033[36m%-23s\033[0m %s\n", \
|
||||
helpCommand, helpMessage"\n"; \
|
||||
helpMessage = ""; \
|
||||
} \
|
||||
} else if ($$0 ~ /^##/) { \
|
||||
if (helpMessage) { \
|
||||
helpMessage = helpMessage"\n "substr($$0, 3); \
|
||||
} else { \
|
||||
helpMessage = substr($$0, 3); \
|
||||
} \
|
||||
} else { \
|
||||
if (helpMessage) { \
|
||||
print "\n "helpMessage"\n" \
|
||||
} \
|
||||
helpMessage = ""; \
|
||||
} \
|
||||
}' \
|
||||
$(MAKEFILE_LIST)
|
||||
|
||||
102
README.md
102
README.md
@ -1,59 +1,95 @@
|
||||
[](https://travis-ci.org/foomo/contentserver)
|
||||
[](https://github.com/foomo/contentserver/actions/workflows/pr.yml)
|
||||
[](https://goreportcard.com/report/github.com/foomo/contentserver)
|
||||
[](https://coveralls.io/github/foomo/contentserver?branch=main)
|
||||
[](https://godoc.org/github.com/foomo/contentserver)
|
||||
|
||||
<p align="center">
|
||||
<img alt="sesamy" src=".github/assets/contentserver.png"/>
|
||||
</p>
|
||||
|
||||
# Content Server
|
||||
|
||||
Serves content tree structures very quickly through a json socket api
|
||||
Serves content tree structures very quickly.
|
||||
|
||||
## Concept
|
||||
|
||||
A Server written in GoLang to mix and resolve content from different content sources, e.g. CMS, Blog, Shop and many other more. The server provides a simple to use API for non blocking content repository updates, to resolve site content by an URI or to get deep-linking multilingual URIs for a given contentID.
|
||||
A Server written in GoLang to mix and resolve content from different content sources, e.g. CMS, Blog, Shop and many
|
||||
other more. The server provides a simple to use API for non blocking content repository updates, to resolve site content
|
||||
by an URI or to get deep-linking multilingual URIs for a given contentID.
|
||||
|
||||
It's up to you how you use it and which data you want to export to the server. Our intention was to write a fast and cache hazzle-free content server to mix different content sources.
|
||||
It's up to you how you use it and which data you want to export to the server. Our intention was to write a fast and
|
||||
cache hazzle-free content server to mix different content sources.
|
||||
|
||||
### Overview
|
||||
|
||||
<img src="docs/assets/Overview.svg" width="100%" height="500">
|
||||
|
||||
## Export Data
|
||||
|
||||
All you have to do is to provide a tree of content nodes as a JSON encoded RepoNode.
|
||||
|
||||
| Attribute | Type | Usage |
|
||||
|---------------|:----------------------:|----------------------------------------------------------------------------------:|
|
||||
| Id | string | unique id to identify the node |
|
||||
| MimeType | string | mime-type of the node, e.g. text/html, image/png, ... |
|
||||
| LinkId | string | (symbolic) link/alias to another node |
|
||||
| Groups | []string | access control |
|
||||
| URI | string | URI |
|
||||
| Name | string | name |
|
||||
| Hidden | bool | hide in menu |
|
||||
| DestinationId | string | alias / symlink handling |
|
||||
| Data | map[string]interface{} | payload data |
|
||||
| Nodes | map[string]*RepoNode | child nodes |
|
||||
| Index | []string | contains the order of of nodes |
|
||||
| Attribute | Type | Usage |
|
||||
|---------------|:----------------------:|------------------------------------------------------:|
|
||||
| Id | string | unique id to identify the node |
|
||||
| MimeType | string | mime-type of the node, e.g. text/html, image/png, ... |
|
||||
| LinkId | string | (symbolic) link/alias to another node |
|
||||
| Groups | []string | access control |
|
||||
| URI | string | URI |
|
||||
| Name | string | name |
|
||||
| Hidden | bool | hide in menu |
|
||||
| DestinationId | string | alias / symlink handling |
|
||||
| Data | map[string]interface{} | payload data |
|
||||
| Nodes | map[string]*RepoNode | child nodes |
|
||||
| Index | []string | contains the order of of nodes |
|
||||
|
||||
### Tips
|
||||
|
||||
- If you do not want to build a multi-market website define a generic market, e.g. call it *universe*
|
||||
- keep it lean and do not export content which should not be accessible at all, e.g. you are working on a super secret fancy new category of your website
|
||||
- Hidden nodes can be resolved by their uri, but are hidden on nodes
|
||||
- To avoid duplicate content provide a DestinationId ( = ContentId of the node you want to reference) instead of URIs
|
||||
- If you do not want to build a multi-market website define a generic market, e.g. call it *universe*
|
||||
- keep it lean and do not export content which should not be accessible at all, e.g. you are working on a super secret
|
||||
fancy new category of your website
|
||||
- Hidden nodes can be resolved by their uri, but are hidden on nodes
|
||||
- To avoid duplicate content provide a DestinationId ( = ContentId of the node you want to reference) instead of URIs
|
||||
|
||||
## Request Data
|
||||
|
||||
There is a PHP Proxy implementation for foomo in [Foomo.ContentServer](https://github.com/foomo/Foomo.ContentServer). Feel free to use it or to implement your own proxy in the language you love. The API should be easily to implement in every other framework and language, too.
|
||||
There is a PHP Proxy implementation for foomo in [Foomo.ContentServer](https://github.com/foomo/Foomo.ContentServer).
|
||||
Feel free to use it or to implement your own proxy in the language you love. The API should be easily to implement in
|
||||
every other framework and language, too.
|
||||
|
||||
## Update Flowchart
|
||||
|
||||
<img src="docs/assets/Update-Flow.svg" width="100%" height="700">
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
$ contentserver --help
|
||||
Usage of contentserver:
|
||||
-address string
|
||||
address to bind host:port (default "127.0.0.1:8081")
|
||||
-log-level string
|
||||
one of error, record, warning, notice, debug (default "record")
|
||||
-var-dir string
|
||||
where to put my data (default "/var/lib/contentserver")
|
||||
-version
|
||||
version info
|
||||
$ contentserver -h
|
||||
Serves content tree structures very quickly
|
||||
|
||||
Usage:
|
||||
contentserver [command]
|
||||
|
||||
Available Commands:
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
help Help about any command
|
||||
http Start http server
|
||||
socket Start socket server
|
||||
version Print version information
|
||||
|
||||
Flags:
|
||||
-h, --help help for contentserver
|
||||
--log-format string log format (default "json")
|
||||
--log-level string log level (default "info")
|
||||
|
||||
Use "contentserver [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Please refer to the [CONTRIBUTING](.github/CONTRIBUTING.md) details and follow the [CODE_OF_CONDUCT](.github/CODE_OF_CONDUCT.md) and [SECURITY](.github/SECURITY.md) guidelines.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) foomo under the LGPL 3.0 license.
|
||||
Distributed under LGPL 3.0 License, please see license file within the code for more details.
|
||||
|
||||
_Made with ♥ [foomo](https://www.foomo.org) by [bestbytes](https://www.bestbytes.com)_
|
||||
|
||||
16
build/buildx.Dockerfile
Normal file
16
build/buildx.Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM alpine:3.21.3
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
RUN addgroup --system --gid 1001 contentserver
|
||||
RUN adduser --system --uid 1001 contentserver
|
||||
|
||||
COPY contentserver /usr/bin/
|
||||
|
||||
RUN mkdir "/var/lib/contentserver" && \
|
||||
chmod 0700 "/var/lib/contentserver" && \
|
||||
chown contentserver:contentserver "/var/lib/contentserver"
|
||||
|
||||
USER contentserver
|
||||
|
||||
ENTRYPOINT ["contentserver"]
|
||||
190
client/client.go
190
client/client.go
@ -1,155 +1,105 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
"github.com/foomo/contentserver/server"
|
||||
)
|
||||
|
||||
type serverResponse struct {
|
||||
Reply interface{}
|
||||
}
|
||||
var (
|
||||
ErrEmptyServerURL = errors.New("empty contentserver url provided")
|
||||
ErrInvalidServerURL = errors.New("invalid contentserver url provided")
|
||||
)
|
||||
|
||||
// Client a content server client
|
||||
type Client struct {
|
||||
Server string
|
||||
conn net.Conn
|
||||
t Transport
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func New(transport Transport) *Client {
|
||||
return &Client{
|
||||
t: transport,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// Update tell the server to update itself
|
||||
func (c *Client) Update() (response *responses.Update, err error) {
|
||||
response = &responses.Update{}
|
||||
err = c.call(server.HandlerUpdate, &requests.Update{}, response)
|
||||
return
|
||||
func (c *Client) Update(ctx context.Context) (*responses.Update, error) {
|
||||
type serverResponse struct {
|
||||
Reply *responses.Update
|
||||
}
|
||||
resp := serverResponse{}
|
||||
if err := c.t.Call(ctx, handler.RouteUpdate, &requests.Update{}, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Reply, nil
|
||||
}
|
||||
|
||||
// GetContent request site content
|
||||
func (c *Client) GetContent(request *requests.Content) (response *content.SiteContent, err error) {
|
||||
response = &content.SiteContent{}
|
||||
err = c.call(server.HandlerGetContent, request, response)
|
||||
return
|
||||
func (c *Client) GetContent(ctx context.Context, request *requests.Content) (*content.SiteContent, error) {
|
||||
type serverResponse struct {
|
||||
Reply *content.SiteContent
|
||||
}
|
||||
resp := serverResponse{}
|
||||
if err := c.t.Call(ctx, handler.RouteGetContent, request, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Reply, nil
|
||||
}
|
||||
|
||||
// GetURIs resolve uris for ids in a dimension
|
||||
func (c *Client) GetURIs(dimension string, IDs []string) (uriMap map[string]string, err error) {
|
||||
uriMap = map[string]string{}
|
||||
err = c.call(
|
||||
server.HandlerGetURIs,
|
||||
&requests.URIs{
|
||||
Dimension: dimension,
|
||||
IDs: IDs,
|
||||
},
|
||||
&uriMap,
|
||||
)
|
||||
return
|
||||
func (c *Client) GetURIs(ctx context.Context, dimension string, ids []string) (map[string]string, error) {
|
||||
type serverResponse struct {
|
||||
Reply map[string]string
|
||||
}
|
||||
|
||||
resp := serverResponse{}
|
||||
if err := c.t.Call(ctx, handler.RouteGetURIs, &requests.URIs{Dimension: dimension, IDs: ids}, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Reply, nil
|
||||
}
|
||||
|
||||
// GetNodes request nodes
|
||||
func (c *Client) GetNodes(env *requests.Env, nodes map[string]*requests.Node) (nodesResponse map[string]*content.Node, err error) {
|
||||
func (c *Client) GetNodes(ctx context.Context, env *requests.Env, nodes map[string]*requests.Node) (map[string]*content.Node, error) {
|
||||
r := &requests.Nodes{
|
||||
Env: env,
|
||||
Nodes: nodes,
|
||||
}
|
||||
nodesResponse = map[string]*content.Node{}
|
||||
err = c.call(server.HandlerGetNodes, r, &nodesResponse)
|
||||
return
|
||||
type serverResponse struct {
|
||||
Reply map[string]*content.Node
|
||||
}
|
||||
resp := serverResponse{}
|
||||
if err := c.t.Call(ctx, handler.RouteGetNodes, r, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Reply, nil
|
||||
}
|
||||
|
||||
// GetRepo get the whole repo
|
||||
func (c *Client) GetRepo() (response map[string]*content.RepoNode, err error) {
|
||||
response = map[string]*content.RepoNode{}
|
||||
err = c.call(server.HandlerGetRepo, &requests.Repo{}, &response)
|
||||
return
|
||||
func (c *Client) GetRepo(ctx context.Context) (map[string]*content.RepoNode, error) {
|
||||
type serverResponse struct {
|
||||
Reply map[string]*content.RepoNode
|
||||
}
|
||||
resp := serverResponse{}
|
||||
if err := c.t.Call(ctx, handler.RouteGetRepo, &requests.Repo{}, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Reply, nil
|
||||
}
|
||||
|
||||
// func (c *Client) closeConnection() error {
|
||||
// if c.conn != nil {
|
||||
// err := c.conn.Close()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// c.conn = nil
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// func (c *Client) getConnection() (conn net.Conn, err error) {
|
||||
// // we need some pooling here
|
||||
// return
|
||||
// }
|
||||
|
||||
func (c *Client) call(handler server.Handler, request interface{}, response interface{}) error {
|
||||
jsonBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal request : %q", err)
|
||||
}
|
||||
conn, err := net.Dial("tcp", c.Server)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can not call server - connection error: %q", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
// write header result will be like handler:2{}
|
||||
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", handler, len(jsonBytes))), jsonBytes...)
|
||||
|
||||
// send request
|
||||
written := 0
|
||||
l := len(jsonBytes)
|
||||
for written < l {
|
||||
n, err := conn.Write(jsonBytes[written:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %q", err)
|
||||
}
|
||||
written += n
|
||||
}
|
||||
|
||||
// read response
|
||||
responseBytes := []byte{}
|
||||
buf := make([]byte, 4096)
|
||||
responseLength := 0
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("an error occured while reading the response: %q", err)
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
responseBytes = append(responseBytes, buf[0:n]...)
|
||||
if responseLength == 0 {
|
||||
for index, byte := range responseBytes {
|
||||
if byte == 123 {
|
||||
// opening bracket
|
||||
responseLength, err = strconv.Atoi(string(responseBytes[0:index]))
|
||||
if err != nil {
|
||||
return errors.New("could not read response length: " + err.Error())
|
||||
}
|
||||
responseBytes = responseBytes[index:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if responseLength > 0 && len(responseBytes) == responseLength {
|
||||
break
|
||||
}
|
||||
}
|
||||
// unmarshal response
|
||||
responseJSONErr := json.Unmarshal(responseBytes, &serverResponse{Reply: response})
|
||||
if responseJSONErr != nil {
|
||||
// is it an error ?
|
||||
remoteErr := responses.Error{}
|
||||
remoteErrJSONErr := json.Unmarshal(responseBytes, remoteErr)
|
||||
if remoteErrJSONErr == nil {
|
||||
return remoteErr
|
||||
}
|
||||
return fmt.Errorf("could not unmarshal response : %q %q", remoteErrJSONErr, string(responseBytes))
|
||||
}
|
||||
return nil
|
||||
func (c *Client) Close() {
|
||||
c.t.Close()
|
||||
}
|
||||
|
||||
@ -1,145 +1,174 @@
|
||||
package client
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/client"
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/log"
|
||||
"github.com/foomo/contentserver/repo/mock"
|
||||
"github.com/foomo/contentserver/server"
|
||||
"github.com/foomo/contentserver/pkg/repo"
|
||||
"github.com/foomo/contentserver/pkg/repo/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
var testServerIsRunning = false
|
||||
|
||||
func dump(t *testing.T, v interface{}) {
|
||||
jsonBytes, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal("could not dump v", v, "err", err)
|
||||
return
|
||||
}
|
||||
t.Log(string(jsonBytes))
|
||||
}
|
||||
|
||||
func getTestClient(t testing.TB) *Client {
|
||||
log.SelectedLevel = log.LevelError
|
||||
addr := "127.0.0.1:9999"
|
||||
if !testServerIsRunning {
|
||||
testServerIsRunning = true
|
||||
testServer, varDir := mock.GetMockData(t)
|
||||
go server.Run(testServer.URL+"/repo-two-dimensions.json", addr, varDir)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
return &Client{
|
||||
Server: addr,
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
c := getTestClient(t)
|
||||
response, err := c.Update()
|
||||
if err != nil {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if !response.Success {
|
||||
t.Fatal("update has to return .Sucesss true", response)
|
||||
}
|
||||
stats := response.Stats
|
||||
if !(stats.RepoRuntime > float64(0.0)) || !(stats.OwnRuntime > float64(0.0)) {
|
||||
t.Fatal("stats invalid")
|
||||
}
|
||||
testWithClients(t, func(t *testing.T, c *client.Client) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
response, err := c.Update(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.True(t, response.Success, "update has to return .Sucesss true")
|
||||
assert.Greater(t, response.Stats.OwnRuntime, 0.0)
|
||||
assert.Greater(t, response.Stats.RepoRuntime, 0.0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetURIs(t *testing.T) {
|
||||
c := getTestClient(t)
|
||||
request := mock.MakeValidURIsRequest()
|
||||
uriMap, err := c.GetURIs(request.Dimension, request.IDs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if uriMap[request.IDs[0]] != "/a" {
|
||||
t.Fatal(uriMap)
|
||||
}
|
||||
testWithClients(t, func(t *testing.T, c *client.Client) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
request := mock.MakeValidURIsRequest()
|
||||
uriMap, err := c.GetURIs(t.Context(), request.Dimension, request.IDs)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/a", uriMap[request.IDs[0]])
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRepo(t *testing.T) {
|
||||
c := getTestClient(t)
|
||||
r, err := c.GetRepo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64) != float64(1) {
|
||||
t.Fatal("failed to drill deep for data")
|
||||
}
|
||||
testWithClients(t, func(t *testing.T, c *client.Client) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
r, err := c.GetRepo(t.Context())
|
||||
require.NoError(t, err)
|
||||
if assert.NotEmpty(t, r, "received empty JSON from GetRepo") {
|
||||
assert.InDelta(t, 1.0, r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64), 0, "failed to drill deep for data") //nolint:forcetypeassert
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetNodes(t *testing.T) {
|
||||
c := getTestClient(t)
|
||||
nodesRequest := mock.MakeNodesRequest()
|
||||
nodes, err := c.GetNodes(nodesRequest.Env, nodesRequest.Nodes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testNode, ok := nodes["test"]
|
||||
if !ok {
|
||||
t.Fatal("that should be a node")
|
||||
}
|
||||
testData, ok := testNode.Item.Data["foo"]
|
||||
if !ok {
|
||||
t.Fatal("where is foo")
|
||||
}
|
||||
if testData != "bar" {
|
||||
t.Fatal("testData should have bennd bar not", testData)
|
||||
}
|
||||
|
||||
testWithClients(t, func(t *testing.T, c *client.Client) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
nodesRequest := mock.MakeNodesRequest()
|
||||
nodes, err := c.GetNodes(t.Context(), nodesRequest.Env, nodesRequest.Nodes)
|
||||
require.NoError(t, err)
|
||||
testNode, ok := nodes["test"]
|
||||
if !ok {
|
||||
t.Fatal("that should be a node")
|
||||
}
|
||||
testData, ok := testNode.Item.Data["foo"]
|
||||
if !ok {
|
||||
t.Fatal("where is foo")
|
||||
}
|
||||
if testData != "bar" {
|
||||
t.Fatal("testData should have bennd bar not", testData)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetContent(t *testing.T) {
|
||||
c := getTestClient(t)
|
||||
request := mock.MakeValidContentRequest()
|
||||
response, err := c.GetContent(request)
|
||||
if err != nil {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if request.URI != response.URI {
|
||||
dump(t, request)
|
||||
dump(t, response)
|
||||
t.Fatal("uri mismatch")
|
||||
}
|
||||
if response.Status != content.StatusOk {
|
||||
t.Fatal("unexpected status")
|
||||
testWithClients(t, func(t *testing.T, c *client.Client) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
request := mock.MakeValidContentRequest()
|
||||
response, err := c.GetContent(t.Context(), request)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, request.URI, response.URI)
|
||||
assert.Equal(t, content.StatusOk, response.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func benchmarkServerAndClientGetContent(b *testing.B, numGroups, numCalls int, client GetContentClient) {
|
||||
b.Helper()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
start := time.Now()
|
||||
benchmarkClientAndServerGetContent(b, numGroups, numCalls, client)
|
||||
dur := time.Since(start)
|
||||
totalCalls := numGroups * numCalls
|
||||
b.Log("requests per second", int(float64(totalCalls)/(float64(dur)/float64(1000000000))), dur, totalCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// not very meaningful yet
|
||||
func BenchmarkServerAndClient(b *testing.B) {
|
||||
func benchmarkClientAndServerGetContent(tb testing.TB, numGroups, numCalls int, client GetContentClient) {
|
||||
tb.Helper()
|
||||
var wg sync.WaitGroup
|
||||
stats := make([]int, 100)
|
||||
for group := 0; group < 100; group++ {
|
||||
wg.Add(1)
|
||||
go func(g int) {
|
||||
wg.Add(numGroups)
|
||||
for group := 0; group < numGroups; group++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c := getTestClient(b)
|
||||
request := mock.MakeValidContentRequest()
|
||||
for i := 0; i < 1000; i++ {
|
||||
response, err := c.GetContent(request)
|
||||
if err != nil {
|
||||
b.Fatal("unexpected err", err)
|
||||
for i := 0; i < numCalls; i++ {
|
||||
response, err := client.GetContent(tb.Context(), request)
|
||||
if err == nil {
|
||||
if request.URI != response.URI {
|
||||
tb.Fatal("uri mismatch")
|
||||
}
|
||||
if response.Status != content.StatusOk {
|
||||
tb.Fatal("unexpected status")
|
||||
}
|
||||
}
|
||||
if request.URI != response.URI {
|
||||
b.Fatal("uri mismatch")
|
||||
}
|
||||
if response.Status != content.StatusOk {
|
||||
b.Fatal("unexpected status")
|
||||
}
|
||||
stats[g] = i
|
||||
}
|
||||
}(group)
|
||||
|
||||
}()
|
||||
}
|
||||
// Wait for all HTTP fetches to complete.
|
||||
wg.Wait()
|
||||
b.Log(stats)
|
||||
}
|
||||
|
||||
func testWithClients(t *testing.T, testFunc func(t *testing.T, c *client.Client)) {
|
||||
t.Helper()
|
||||
t.Run("http", func(t *testing.T) {
|
||||
l := zaptest.NewLogger(t)
|
||||
s := initHTTPRepoServer(t, l)
|
||||
c := newHTTPClient(t, s)
|
||||
defer func() {
|
||||
s.Close()
|
||||
c.Close()
|
||||
}()
|
||||
testFunc(t, c)
|
||||
})
|
||||
t.Run("socket", func(t *testing.T) {
|
||||
l := zaptest.NewLogger(t)
|
||||
s := initSocketRepoServer(t, l)
|
||||
c := newSocketClient(t, s.Addr().String())
|
||||
defer func() {
|
||||
s.Close()
|
||||
c.Close()
|
||||
}()
|
||||
testFunc(t, c)
|
||||
})
|
||||
}
|
||||
|
||||
func initRepo(tb testing.TB, l *zap.Logger) *repo.Repo {
|
||||
tb.Helper()
|
||||
testRepoServer, varDir := mock.GetMockData(tb)
|
||||
r := repo.New(l,
|
||||
testRepoServer.URL+"/repo-two-dimensions.json",
|
||||
repo.NewHistory(l,
|
||||
repo.HistoryWithHistoryDir(varDir),
|
||||
),
|
||||
)
|
||||
up := make(chan bool, 1)
|
||||
r.OnLoaded(func() {
|
||||
up <- true
|
||||
})
|
||||
go r.Start(tb.Context()) //nolint:errcheck
|
||||
<-up
|
||||
return r
|
||||
}
|
||||
|
||||
// func dump(t *testing.T, v interface{}) {
|
||||
// t.Helper()
|
||||
// jsonBytes, err := json.MarshalIndent(v, "", " ")
|
||||
// if err != nil {
|
||||
// t.Fatal("could not dump v", v, "err", err)
|
||||
// return
|
||||
// }
|
||||
// t.Log(string(jsonBytes))
|
||||
// }
|
||||
|
||||
134
client/connectionpool.go
Normal file
134
client/connectionpool.go
Normal file
@ -0,0 +1,134 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type connectionPool struct {
|
||||
url string
|
||||
// conn net.Conn
|
||||
chanConnGet chan chan net.Conn
|
||||
chanConnReturn chan connReturn
|
||||
chanDrainPool chan int
|
||||
}
|
||||
|
||||
func newConnectionPool(url string, connectionPoolSize int, waitTimeout time.Duration) *connectionPool {
|
||||
connPool := &connectionPool{
|
||||
url: url,
|
||||
chanConnGet: make(chan chan net.Conn),
|
||||
chanConnReturn: make(chan connReturn),
|
||||
chanDrainPool: make(chan int),
|
||||
}
|
||||
go connPool.run(connectionPoolSize, waitTimeout)
|
||||
return connPool
|
||||
}
|
||||
|
||||
func (c *connectionPool) run(connectionPoolSize int, waitTimeout time.Duration) {
|
||||
type poolEntry struct {
|
||||
busy bool
|
||||
err error
|
||||
conn net.Conn
|
||||
}
|
||||
type waitPoolEntry struct {
|
||||
entryTime time.Time
|
||||
chanConn chan net.Conn
|
||||
}
|
||||
|
||||
var (
|
||||
connectionPool = make(map[int]*poolEntry, connectionPoolSize)
|
||||
waitPool = map[int]*waitPoolEntry{}
|
||||
)
|
||||
for i := 0; i < connectionPoolSize; i++ {
|
||||
connectionPool[i] = &poolEntry{
|
||||
conn: nil,
|
||||
busy: false,
|
||||
}
|
||||
}
|
||||
RunLoop:
|
||||
for {
|
||||
// fmt.Println("----------------------- run loop ------------------------")
|
||||
select {
|
||||
case <-c.chanDrainPool:
|
||||
// fmt.Println("<-c.chanDrainPool")
|
||||
for _, waitPoolEntry := range waitPool {
|
||||
waitPoolEntry.chanConn <- nil
|
||||
}
|
||||
break RunLoop
|
||||
case <-time.After(waitTimeout):
|
||||
// fmt.Println("tick", len(connectionPool), len(waitPool))
|
||||
// for i, poolEntry := range connectionPool {
|
||||
// fmt.Println(i, poolEntry)
|
||||
// }
|
||||
// for i, waitPoolEntry := range waitPool {
|
||||
// fmt.Println(i, waitPoolEntry)
|
||||
// }
|
||||
case chanReturnNextConn := <-c.chanConnGet:
|
||||
// fmt.Println("chanReturnNextConn := <-c.chanConnGet:")
|
||||
nextI := 0
|
||||
for i := range waitPool {
|
||||
if i >= nextI {
|
||||
nextI = i + 1
|
||||
}
|
||||
}
|
||||
waitPool[nextI] = &waitPoolEntry{
|
||||
chanConn: chanReturnNextConn,
|
||||
entryTime: time.Now(),
|
||||
}
|
||||
// fmt.Println("sbdy wants a new conn", nextI)
|
||||
case connReturn := <-c.chanConnReturn:
|
||||
// fmt.Println("connReturn := <-c.chanConnReturn:")
|
||||
for _, poolEntry := range connectionPool {
|
||||
if connReturn.conn == poolEntry.conn {
|
||||
poolEntry.busy = false
|
||||
if connReturn.err != nil {
|
||||
poolEntry.err = connReturn.err
|
||||
poolEntry.conn.Close()
|
||||
poolEntry.conn = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// refill connection pool
|
||||
for _, poolEntry := range connectionPool {
|
||||
if poolEntry.conn == nil {
|
||||
newConn, errDial := net.Dial("tcp", c.url)
|
||||
poolEntry.err = errDial
|
||||
poolEntry.conn = newConn
|
||||
}
|
||||
}
|
||||
// redistribute available connections
|
||||
for _, poolEntry := range connectionPool {
|
||||
if len(waitPool) == 0 {
|
||||
break
|
||||
}
|
||||
if poolEntry.err == nil && poolEntry.conn != nil && !poolEntry.busy {
|
||||
for i, waitPoolEntry := range waitPool {
|
||||
// fmt.Println("---------------------------> serving wait pool", i, waitPoolEntry)
|
||||
poolEntry.busy = true
|
||||
delete(waitPool, i)
|
||||
waitPoolEntry.chanConn <- poolEntry.conn
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// waitpool cleanup
|
||||
var (
|
||||
waitPoolLoosers = []int{}
|
||||
now = time.Now()
|
||||
)
|
||||
for i, waitPoolEntry := range waitPool {
|
||||
if now.Sub(waitPoolEntry.entryTime) > waitTimeout {
|
||||
waitPoolLoosers = append(waitPoolLoosers, i)
|
||||
waitPoolEntry.chanConn <- nil
|
||||
}
|
||||
}
|
||||
for _, i := range waitPoolLoosers {
|
||||
delete(waitPool, i)
|
||||
}
|
||||
}
|
||||
c.chanDrainPool = nil
|
||||
c.chanConnReturn = nil
|
||||
c.chanConnGet = nil
|
||||
// fmt.Println("runloop is done", waitPool)
|
||||
}
|
||||
105
client/httptransport.go
Normal file
105
client/httptransport.go
Normal file
@ -0,0 +1,105 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/pkg/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
HTTPTransport struct {
|
||||
httpClient *http.Client
|
||||
endpoint string
|
||||
}
|
||||
HTTPTransportOption func(*HTTPTransport)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// NewHTTPTransport will create a new http transport for the given server and client.
|
||||
// Caution: the provided server url is not validated!
|
||||
func NewHTTPTransport(server string, opts ...HTTPTransportOption) *HTTPTransport {
|
||||
inst := &HTTPTransport{
|
||||
endpoint: server,
|
||||
httpClient: http.DefaultClient,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(inst)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
// NewHTTPClient constructs a new client to talk to the contentserver.
|
||||
// It returns an error if the provided url is empty or invalid.
|
||||
func NewHTTPClient(url string) (c *Client, err error) {
|
||||
if url == "" {
|
||||
return nil, ErrEmptyServerURL
|
||||
}
|
||||
|
||||
// validate url
|
||||
if !utils.IsValidURL(url) {
|
||||
return nil, ErrInvalidServerURL
|
||||
}
|
||||
|
||||
return New(NewHTTPTransport(url)), nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Options
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func HTTPTransportWithHTTPClient(v *http.Client) HTTPTransportOption {
|
||||
return func(o *HTTPTransport) {
|
||||
o.httpClient = v
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (t *HTTPTransport) Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error {
|
||||
requestBytes, errMarshal := json.Marshal(request)
|
||||
if errMarshal != nil {
|
||||
return errMarshal
|
||||
}
|
||||
req, errNewRequest := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
t.endpoint+"/"+string(route),
|
||||
bytes.NewBuffer(requestBytes),
|
||||
)
|
||||
if errNewRequest != nil {
|
||||
return errNewRequest
|
||||
}
|
||||
httpResponse, errDo := t.httpClient.Do(req)
|
||||
if errDo != nil {
|
||||
return errDo
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
if httpResponse.StatusCode != http.StatusOK {
|
||||
return errors.New("non 200 reply")
|
||||
}
|
||||
if httpResponse.Body == nil {
|
||||
return errors.New("empty response body")
|
||||
}
|
||||
responseBytes, errRead := io.ReadAll(httpResponse.Body)
|
||||
if errRead != nil {
|
||||
return errRead
|
||||
}
|
||||
return json.Unmarshal(responseBytes, response)
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) Close() {
|
||||
// nothing to do here
|
||||
}
|
||||
64
client/httptransport_test.go
Normal file
64
client/httptransport_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/foomo/contentserver/client"
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
const pathContentserver = "/contentserver"
|
||||
|
||||
func TestInvalidHTTPClientInit(t *testing.T) {
|
||||
c, err := client.NewHTTPClient("")
|
||||
assert.Nil(t, c)
|
||||
require.Error(t, err)
|
||||
|
||||
c, err = client.NewHTTPClient("bogus")
|
||||
assert.Nil(t, c)
|
||||
require.Error(t, err)
|
||||
|
||||
c, err = client.NewHTTPClient("htt:/notaurl")
|
||||
assert.Nil(t, c)
|
||||
require.Error(t, err)
|
||||
|
||||
c, err = client.NewHTTPClient("htts://notaurl")
|
||||
assert.Nil(t, c)
|
||||
require.Error(t, err)
|
||||
|
||||
c, err = client.NewHTTPClient("/path/segment/only")
|
||||
assert.Nil(t, c)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func BenchmarkWebClientAndServerGetContent(b *testing.B) {
|
||||
l := zaptest.NewLogger(b)
|
||||
server := initHTTPRepoServer(b, l)
|
||||
httpClient := newHTTPClient(b, server)
|
||||
benchmarkServerAndClientGetContent(b, 30, 100, httpClient)
|
||||
}
|
||||
|
||||
type GetContentClient interface {
|
||||
GetContent(ctx context.Context, request *requests.Content) (response *content.SiteContent, err error)
|
||||
}
|
||||
|
||||
func newHTTPClient(tb testing.TB, server *httptest.Server) *client.Client {
|
||||
tb.Helper()
|
||||
c, err := client.NewHTTPClient(server.URL + pathContentserver)
|
||||
require.NoError(tb, err)
|
||||
return c
|
||||
}
|
||||
|
||||
func initHTTPRepoServer(tb testing.TB, l *zap.Logger) *httptest.Server {
|
||||
tb.Helper()
|
||||
r := initRepo(tb, l)
|
||||
return httptest.NewServer(handler.NewHTTP(l, r))
|
||||
}
|
||||
7
client/json.go
Normal file
7
client/json.go
Normal file
@ -0,0 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
133
client/sockettransport.go
Normal file
133
client/sockettransport.go
Normal file
@ -0,0 +1,133 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
)
|
||||
|
||||
type connReturn struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
type SocketTransport struct {
|
||||
connPool *connectionPool
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func NewSocketTransport(url string, connectionPoolSize int, waitTimeout time.Duration) *SocketTransport {
|
||||
return &SocketTransport{
|
||||
connPool: newConnectionPool(url, connectionPoolSize, waitTimeout),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (t *SocketTransport) Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error {
|
||||
if t.connPool.chanDrainPool == nil {
|
||||
return errors.New("connection pool has been drained, client is dead")
|
||||
}
|
||||
jsonBytes, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal request : %q", err)
|
||||
}
|
||||
netChan := make(chan net.Conn)
|
||||
t.connPool.chanConnGet <- netChan
|
||||
conn := <-netChan
|
||||
if conn == nil {
|
||||
return errors.New("could not get a connection")
|
||||
}
|
||||
returnConn := func(err error) {
|
||||
t.connPool.chanConnReturn <- connReturn{
|
||||
conn: conn,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
// write header result will be like handler:2{}
|
||||
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", route, len(jsonBytes))), jsonBytes...)
|
||||
|
||||
// send request
|
||||
var (
|
||||
written = 0
|
||||
l = len(jsonBytes)
|
||||
)
|
||||
for written < l {
|
||||
n, err := conn.Write(jsonBytes[written:])
|
||||
if err != nil {
|
||||
returnConn(err)
|
||||
return fmt.Errorf("failed to send request: %q", err)
|
||||
}
|
||||
written += n
|
||||
}
|
||||
|
||||
// read response
|
||||
var (
|
||||
responseBytes = []byte{}
|
||||
buf = make([]byte, 4096)
|
||||
responseLength = 0
|
||||
)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
returnConn(err)
|
||||
return fmt.Errorf("an error occurred while reading the response: %q", err)
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
responseBytes = append(responseBytes, buf[0:n]...)
|
||||
if responseLength == 0 {
|
||||
for index, byte := range responseBytes {
|
||||
if byte == 123 {
|
||||
// opening bracket
|
||||
responseLength, err = strconv.Atoi(string(responseBytes[0:index]))
|
||||
if err != nil {
|
||||
returnConn(err)
|
||||
return errors.New("could not read response length: " + err.Error())
|
||||
}
|
||||
responseBytes = responseBytes[index:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if responseLength > 0 && len(responseBytes) == responseLength {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// unmarshal response
|
||||
errResponse := json.Unmarshal(responseBytes, response)
|
||||
if errResponse != nil {
|
||||
// is it an error ?
|
||||
var (
|
||||
remoteErr = responses.Error{}
|
||||
remoteErrJSONErr = json.Unmarshal(responseBytes, &remoteErr)
|
||||
)
|
||||
if remoteErrJSONErr == nil {
|
||||
returnConn(remoteErrJSONErr)
|
||||
return remoteErr
|
||||
}
|
||||
return fmt.Errorf("could not unmarshal response : %q %q", remoteErrJSONErr, string(responseBytes))
|
||||
}
|
||||
returnConn(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *SocketTransport) Close() {
|
||||
if t.connPool.chanDrainPool != nil {
|
||||
t.connPool.chanDrainPool <- 1
|
||||
}
|
||||
}
|
||||
61
client/sockettransport_test.go
Normal file
61
client/sockettransport_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/client"
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"golang.org/x/net/nettest"
|
||||
)
|
||||
|
||||
func BenchmarkSocketClientAndServerGetContent(b *testing.B) {
|
||||
l := zaptest.NewLogger(b)
|
||||
socketServer := initSocketRepoServer(b, l)
|
||||
socketClient := newSocketClient(b, socketServer.Addr().String())
|
||||
defer socketClient.Close()
|
||||
defer socketServer.Close()
|
||||
benchmarkServerAndClientGetContent(b, 30, 100, socketClient)
|
||||
}
|
||||
|
||||
func newSocketClient(tb testing.TB, address string) *client.Client {
|
||||
tb.Helper()
|
||||
return client.New(client.NewSocketTransport(address, 25, 100*time.Millisecond))
|
||||
}
|
||||
|
||||
func initSocketRepoServer(tb testing.TB, l *zap.Logger) net.Listener {
|
||||
tb.Helper()
|
||||
r := initRepo(tb, l)
|
||||
h := handler.NewSocket(l, r)
|
||||
|
||||
// listen on socket
|
||||
ln, err := nettest.NewLocalListener("tcp")
|
||||
require.NoError(tb, err)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// this blocks until connection or error
|
||||
conn, err := ln.Accept()
|
||||
if errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
|
||||
return
|
||||
} else if err != nil {
|
||||
tb.Error("runSocketServer: could not accept connection", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// a goroutine handles conn so that the loop can accept other connections
|
||||
go func() {
|
||||
// l.Debug("accepted connection", zap.String("source", conn.RemoteAddr().String()))
|
||||
h.Serve(conn)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return ln
|
||||
}
|
||||
12
client/transport.go
Normal file
12
client/transport.go
Normal file
@ -0,0 +1,12 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
)
|
||||
|
||||
type Transport interface {
|
||||
Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error
|
||||
Close()
|
||||
}
|
||||
126
cmd/flags.go
Normal file
126
cmd/flags.go
Normal file
@ -0,0 +1,126 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func logLevelFlag(v *viper.Viper) string {
|
||||
return v.GetString("log.level")
|
||||
}
|
||||
|
||||
func addLogLevelFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.String("log-level", "info", "log level")
|
||||
_ = v.BindPFlag("log.level", flags.Lookup("log-level"))
|
||||
_ = v.BindEnv("log.level", "LOG_LEVEL")
|
||||
}
|
||||
|
||||
func logFormatFlag(v *viper.Viper) string {
|
||||
return v.GetString("log.format")
|
||||
}
|
||||
|
||||
func addLogFormatFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.String("log-format", "json", "log format")
|
||||
_ = v.BindPFlag("log.format", flags.Lookup("log-format"))
|
||||
_ = v.BindEnv("log.format", "LOG_FORMAT")
|
||||
}
|
||||
|
||||
func addressFlag(v *viper.Viper) string {
|
||||
return v.GetString("address")
|
||||
}
|
||||
|
||||
func addAddressFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.String("address", ":8080", "Address to bind to (host:port)")
|
||||
_ = v.BindPFlag("address", flags.Lookup("address"))
|
||||
_ = v.BindEnv("address", "CONTENT_SERVER_ADDRESS")
|
||||
}
|
||||
|
||||
func basePathFlag(v *viper.Viper) string {
|
||||
return v.GetString("base_path")
|
||||
}
|
||||
|
||||
func addBasePathFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.String("base-path", "/contentserver", "Base path to export the webserver on")
|
||||
_ = v.BindPFlag("base_path", flags.Lookup("base-path"))
|
||||
_ = v.BindEnv("base_path", "CONTENT_SERVER_BASE_PATH")
|
||||
}
|
||||
|
||||
func pollFlag(v *viper.Viper) bool {
|
||||
return v.GetBool("poll.enabled")
|
||||
}
|
||||
|
||||
func addPollFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Bool("poll", false, "If true, the address arg will be used to periodically poll the content url")
|
||||
_ = v.BindPFlag("poll.enabled", flags.Lookup("poll"))
|
||||
_ = v.BindEnv("poll.enabled", "CONTENT_SERVER_POLL")
|
||||
}
|
||||
|
||||
func pollIntevalFlag(v *viper.Viper) time.Duration {
|
||||
return v.GetDuration("poll.interval")
|
||||
}
|
||||
|
||||
func addPollIntervalFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Duration("poll-interval", time.Minute, "Specifies the poll interval")
|
||||
_ = v.BindPFlag("poll.interval", flags.Lookup("poll-interval"))
|
||||
_ = v.BindEnv("poll.interval", "CONTENT_SERVER_POLL_INTERVAL")
|
||||
}
|
||||
|
||||
func historyDirFlag(v *viper.Viper) string {
|
||||
return v.GetString("history.dir")
|
||||
}
|
||||
|
||||
func addHistoryDirFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.String("history-dir", "/var/lib/contentserver", "Where to put my data")
|
||||
_ = v.BindPFlag("history.dir", flags.Lookup("history-dir"))
|
||||
_ = v.BindEnv("history.dir", "CONTENT_SERVER_HISTORY_DIR")
|
||||
}
|
||||
|
||||
func historyLimitFlag(v *viper.Viper) int {
|
||||
return v.GetInt("history.limit")
|
||||
}
|
||||
|
||||
func addHistoryLimitFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Int("history-limit", 2, "Number of history records to keep")
|
||||
_ = v.BindPFlag("history.limit", flags.Lookup("history-limit"))
|
||||
_ = v.BindEnv("history.limit", "CONTENT_SERVER_HISTORY_LIMIT")
|
||||
}
|
||||
|
||||
func gracefulPeriodFlag(v *viper.Viper) time.Duration {
|
||||
return v.GetDuration("graceful.period")
|
||||
}
|
||||
|
||||
func addShutdownTimeoutFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Duration("graceful-period", 0, "Graceful shutdown period")
|
||||
_ = v.BindPFlag("graceful.period", flags.Lookup("graceful-period"))
|
||||
_ = v.BindEnv("graceful.period", "CONTENT_SERVER_GRACEFULE_PERIOD")
|
||||
}
|
||||
|
||||
func serviceHealthzEnabledFlag(v *viper.Viper) bool {
|
||||
return v.GetBool("service.healthz.enabled")
|
||||
}
|
||||
|
||||
func addServiceHealthzEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Bool("service-healthz-enabled", false, "Enable healthz service")
|
||||
_ = v.BindPFlag("service.healthz.enabled", flags.Lookup("service-healthz-enabled"))
|
||||
}
|
||||
|
||||
func servicePrometheusEnabledFlag(v *viper.Viper) bool {
|
||||
return v.GetBool("service.prometheus.enabled")
|
||||
}
|
||||
|
||||
func addServicePrometheusEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Bool("service-prometheus-enabled", false, "Enable prometheus service")
|
||||
_ = v.BindPFlag("service.prometheus.enabled", flags.Lookup("service-prometheus-enabled"))
|
||||
}
|
||||
|
||||
func otelEnabledFlag(v *viper.Viper) bool {
|
||||
return v.GetBool("otel.enabled")
|
||||
}
|
||||
|
||||
func addOtelEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
|
||||
flags.Bool("otel-enabled", false, "Enable otel service")
|
||||
_ = v.BindPFlag("otel.enabled", flags.Lookup("otel-enabled"))
|
||||
_ = v.BindEnv("otel.enabled", "OTEL_ENABLED")
|
||||
}
|
||||
100
cmd/http.go
Normal file
100
cmd/http.go
Normal file
@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/pkg/repo"
|
||||
"github.com/foomo/keel"
|
||||
"github.com/foomo/keel/healthz"
|
||||
keelhttp "github.com/foomo/keel/net/http"
|
||||
"github.com/foomo/keel/net/http/middleware"
|
||||
"github.com/foomo/keel/service"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewHTTPCommand() *cobra.Command {
|
||||
v := newViper()
|
||||
cmd := &cobra.Command{
|
||||
Use: "http <url>",
|
||||
Short: "Start http server",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var comps []string
|
||||
if len(args) == 0 {
|
||||
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repository you are adding")
|
||||
} else {
|
||||
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
|
||||
}
|
||||
return comps, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
svr := keel.NewServer(
|
||||
keel.WithHTTPPrometheusService(servicePrometheusEnabledFlag(v)),
|
||||
keel.WithHTTPHealthzService(serviceHealthzEnabledFlag(v)),
|
||||
keel.WithPrometheusMeter(servicePrometheusEnabledFlag(v)),
|
||||
keel.WithGracefulPeriod(gracefulPeriodFlag(v)),
|
||||
keel.WithOTLPGRPCTracer(otelEnabledFlag(v)),
|
||||
)
|
||||
|
||||
l := svr.Logger()
|
||||
|
||||
r := repo.New(l.Named("inst.repo"),
|
||||
args[0],
|
||||
repo.NewHistory(l.Named("inst.history"),
|
||||
repo.HistoryWithHistoryDir(historyDirFlag(v)),
|
||||
repo.HistoryWithHistoryLimit(historyLimitFlag(v)),
|
||||
),
|
||||
repo.WithHTTPClient(
|
||||
keelhttp.NewHTTPClient(
|
||||
keelhttp.HTTPClientWithTelemetry(),
|
||||
),
|
||||
),
|
||||
repo.WithPollInterval(pollIntevalFlag(v)),
|
||||
repo.WithPoll(pollFlag(v)),
|
||||
)
|
||||
|
||||
isLoadedHealtherFn := healthz.NewHealthzerFn(func(ctx context.Context) error {
|
||||
if !r.Loaded() {
|
||||
return errors.New("repo not loaded yet")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// start initial update and handle error
|
||||
svr.AddStartupHealthzers(isLoadedHealtherFn)
|
||||
svr.AddReadinessHealthzers(isLoadedHealtherFn)
|
||||
|
||||
svr.AddServices(
|
||||
service.NewGoRoutine(l.Named("go.repo"), "repo", func(ctx context.Context, l *zap.Logger) error {
|
||||
return r.Start(ctx)
|
||||
}),
|
||||
service.NewHTTP(l.Named("svc.http"), "http", addressFlag(v),
|
||||
handler.NewHTTP(l.Named("inst.handler"), r, handler.WithBasePath(basePathFlag(v))),
|
||||
middleware.Telemetry(),
|
||||
middleware.Logger(),
|
||||
middleware.GZip(),
|
||||
middleware.Recover(),
|
||||
),
|
||||
)
|
||||
|
||||
svr.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
addAddressFlag(flags, v)
|
||||
addBasePathFlag(flags, v)
|
||||
addPollFlag(flags, v)
|
||||
addPollIntervalFlag(flags, v)
|
||||
addHistoryDirFlag(flags, v)
|
||||
addHistoryLimitFlag(flags, v)
|
||||
addShutdownTimeoutFlag(flags, v)
|
||||
addOtelEnabledFlag(flags, v)
|
||||
addServiceHealthzEnabledFlag(flags, v)
|
||||
addServicePrometheusEnabledFlag(flags, v)
|
||||
|
||||
return cmd
|
||||
}
|
||||
57
cmd/root.go
Normal file
57
cmd/root.go
Normal file
@ -0,0 +1,57 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/foomo/keel/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NewRootCommand represents the base command when called without any subcommands
|
||||
func NewRootCommand() *cobra.Command {
|
||||
v := newViper()
|
||||
cmd := &cobra.Command{
|
||||
Use: "contentserver",
|
||||
Short: "Serves content tree structures very quickly",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
zap.ReplaceGlobals(log.NewLogger(
|
||||
logLevelFlag(v),
|
||||
logFormatFlag(v),
|
||||
))
|
||||
},
|
||||
}
|
||||
|
||||
addLogLevelFlag(cmd.PersistentFlags(), v)
|
||||
addLogFormatFlag(cmd.PersistentFlags(), v)
|
||||
|
||||
cmd.AddCommand(NewHTTPCommand())
|
||||
cmd.AddCommand(NewSocketCommand())
|
||||
cmd.AddCommand(NewVersionCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := NewRootCommand().Execute(); err != nil {
|
||||
log.Logger().Fatal("failed to run command", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
viper.EnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
}
|
||||
|
||||
func newViper() *viper.Viper {
|
||||
v := viper.New()
|
||||
v.AutomaticEnv()
|
||||
return v
|
||||
}
|
||||
96
cmd/socket.go
Normal file
96
cmd/socket.go
Normal file
@ -0,0 +1,96 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/handler"
|
||||
"github.com/foomo/contentserver/pkg/repo"
|
||||
"github.com/foomo/keel/log"
|
||||
keelhttp "github.com/foomo/keel/net/http"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewSocketCommand() *cobra.Command {
|
||||
v := viper.New()
|
||||
cmd := &cobra.Command{
|
||||
Use: "socket <url>",
|
||||
Short: "Start socket server",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var comps []string
|
||||
if len(args) == 0 {
|
||||
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repository you are adding")
|
||||
} else {
|
||||
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
|
||||
}
|
||||
return comps, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
l := log.Logger()
|
||||
|
||||
r := repo.New(l,
|
||||
args[0],
|
||||
repo.NewHistory(l,
|
||||
repo.HistoryWithHistoryDir(historyDirFlag(v)),
|
||||
repo.HistoryWithHistoryLimit(historyLimitFlag(v)),
|
||||
),
|
||||
repo.WithHTTPClient(
|
||||
keelhttp.NewHTTPClient(
|
||||
keelhttp.HTTPClientWithTelemetry(),
|
||||
),
|
||||
),
|
||||
repo.WithPoll(pollFlag(v)),
|
||||
repo.WithPollInterval(pollIntevalFlag(v)),
|
||||
)
|
||||
|
||||
// create socket server
|
||||
handle := handler.NewSocket(l, r)
|
||||
|
||||
// listen on socket
|
||||
ln, err := net.Listen("tcp", addressFlag(v))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start repo
|
||||
up := make(chan bool, 1)
|
||||
r.OnLoaded(func() {
|
||||
up <- true
|
||||
})
|
||||
go r.Start(context.Background()) //nolint:errcheck
|
||||
<-up
|
||||
|
||||
l.Info("started listening", zap.String("address", addressFlag(v)))
|
||||
|
||||
for {
|
||||
// this blocks until connection or error
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
l.Error("runSocketServer: could not accept connection", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// a goroutine handles conn so that the loop can accept other connections
|
||||
go func() {
|
||||
l.Debug("accepted connection", zap.String("source", conn.RemoteAddr().String()))
|
||||
handle.Serve(conn)
|
||||
if err := conn.Close(); err != nil {
|
||||
l.Warn("failed to close connection", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
addAddressFlag(flags, v)
|
||||
addPollFlag(flags, v)
|
||||
addPollIntervalFlag(flags, v)
|
||||
addHistoryDirFlag(flags, v)
|
||||
addHistoryLimitFlag(flags, v)
|
||||
|
||||
return cmd
|
||||
}
|
||||
21
cmd/version.go
Normal file
21
cmd/version.go
Normal file
@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Populated by goreleaser during build
|
||||
var version = "latest"
|
||||
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(version)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@ -4,6 +4,6 @@ package content
|
||||
const (
|
||||
// Indent for json indentation
|
||||
Indent string = "\t"
|
||||
// PathSeparator seprator for paths in URIs
|
||||
// PathSeparator separator for paths in URIs
|
||||
PathSeparator = "/"
|
||||
)
|
||||
|
||||
@ -6,7 +6,9 @@ type Item struct {
|
||||
Name string `json:"name"`
|
||||
URI string `json:"URI"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// NewItem item contructor
|
||||
|
||||
@ -22,26 +22,26 @@ type RepoNode struct {
|
||||
// published from - to is going to be an array of fromTos
|
||||
}
|
||||
|
||||
// NewRepoNode constructor
|
||||
func NewRepoNode() *RepoNode {
|
||||
return &RepoNode{
|
||||
Data: make(map[string]interface{}),
|
||||
Nodes: make(map[string]*RepoNode),
|
||||
}
|
||||
}
|
||||
// // NewRepoNode constructor
|
||||
// func NewRepoNode() *RepoNode {
|
||||
// return &RepoNode{
|
||||
// Data: make(map[string]interface{}, 0), // set initial size to zero explicitly?
|
||||
// Nodes: make(map[string]*RepoNode, 0),
|
||||
// }
|
||||
// }
|
||||
|
||||
// WireParents helper method to reference from child to parent in a tree
|
||||
// recursively
|
||||
func (node *RepoNode) WireParents() {
|
||||
for _, childNode := range node.Nodes {
|
||||
childNode.parent = node
|
||||
func (n *RepoNode) WireParents() {
|
||||
for _, childNode := range n.Nodes {
|
||||
childNode.parent = n
|
||||
childNode.WireParents()
|
||||
}
|
||||
}
|
||||
|
||||
// InPath is the given node in a path
|
||||
func (node *RepoNode) InPath(path []*Item) bool {
|
||||
myParentID := node.parent.ID
|
||||
func (n *RepoNode) InPath(path []*Item) bool {
|
||||
myParentID := n.parent.ID
|
||||
for _, pathItem := range path {
|
||||
if pathItem.ID == myParentID {
|
||||
return true
|
||||
@ -51,57 +51,73 @@ func (node *RepoNode) InPath(path []*Item) bool {
|
||||
}
|
||||
|
||||
// GetPath get a path for a repo node
|
||||
func (node *RepoNode) GetPath() []*Item {
|
||||
parentNode := node.parent
|
||||
pathLength := 0
|
||||
func (n *RepoNode) GetPath(dataFields []string) []*Item {
|
||||
var (
|
||||
parentNode = n.parent
|
||||
pathLength = 0
|
||||
)
|
||||
for parentNode != nil {
|
||||
parentNode = parentNode.parent
|
||||
pathLength++
|
||||
}
|
||||
parentNode = node.parent
|
||||
i := 0
|
||||
path := make([]*Item, pathLength)
|
||||
parentNode = n.parent
|
||||
|
||||
var (
|
||||
i = 0
|
||||
path = make([]*Item, pathLength)
|
||||
)
|
||||
|
||||
if dataFields == nil {
|
||||
dataFields = []string{}
|
||||
}
|
||||
|
||||
for parentNode != nil {
|
||||
path[i] = parentNode.ToItem([]string{})
|
||||
path[i] = parentNode.ToItem(dataFields)
|
||||
parentNode = parentNode.parent
|
||||
i++
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// ToItem convert a re po node to a simple repo item
|
||||
func (node *RepoNode) ToItem(dataFields []string) *Item {
|
||||
// ToItem convert a repo node to a simple repo item
|
||||
func (n *RepoNode) ToItem(dataFields []string) *Item {
|
||||
item := NewItem()
|
||||
item.ID = node.ID
|
||||
item.Name = node.Name
|
||||
item.MimeType = node.MimeType
|
||||
item.URI = node.URI
|
||||
for _, dataField := range dataFields {
|
||||
if data, ok := node.Data[dataField]; ok {
|
||||
item.Data[dataField] = data
|
||||
item.ID = n.ID
|
||||
item.Name = n.Name
|
||||
item.MimeType = n.MimeType
|
||||
item.Hidden = n.Hidden
|
||||
item.URI = n.URI
|
||||
item.Groups = n.Groups
|
||||
if dataFields == nil {
|
||||
item.Data = n.Data
|
||||
} else {
|
||||
for _, dataField := range dataFields {
|
||||
if data, ok := n.Data[dataField]; ok {
|
||||
item.Data[dataField] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// GetParent get the parent node of a node
|
||||
func (node *RepoNode) GetParent() *RepoNode {
|
||||
return node.parent
|
||||
func (n *RepoNode) GetParent() *RepoNode {
|
||||
return n.parent
|
||||
}
|
||||
|
||||
// AddNode adds a named child node
|
||||
func (node *RepoNode) AddNode(name string, childNode *RepoNode) *RepoNode {
|
||||
node.Nodes[name] = childNode
|
||||
return node
|
||||
func (n *RepoNode) AddNode(name string, childNode *RepoNode) *RepoNode {
|
||||
n.Nodes[name] = childNode
|
||||
return n
|
||||
}
|
||||
|
||||
// IsOneOfTheseMimeTypes is the node one of the given mime types
|
||||
func (node *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
|
||||
func (n *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
|
||||
if len(mimeTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, mimeType := range mimeTypes {
|
||||
if mimeType == node.MimeType {
|
||||
if mimeType == n.MimeType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -110,14 +126,14 @@ func (node *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
|
||||
|
||||
// CanBeAccessedByGroups can this node be accessed by at least one the given
|
||||
// groups
|
||||
func (node *RepoNode) CanBeAccessedByGroups(groups []string) bool {
|
||||
func (n *RepoNode) CanBeAccessedByGroups(groups []string) bool {
|
||||
// no groups set on node => anybody can access it
|
||||
if len(node.Groups) == 0 {
|
||||
if len(n.Groups) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
for _, myGroup := range node.Groups {
|
||||
for _, myGroup := range n.Groups {
|
||||
if group == myGroup {
|
||||
return true
|
||||
}
|
||||
@ -127,10 +143,10 @@ func (node *RepoNode) CanBeAccessedByGroups(groups []string) bool {
|
||||
}
|
||||
|
||||
// PrintNode essentially a recursive dump
|
||||
func (node *RepoNode) PrintNode(id string, level int) {
|
||||
func (n *RepoNode) PrintNode(id string, level int) {
|
||||
prefix := strings.Repeat(Indent, level)
|
||||
fmt.Printf("%s %s %s:\n", prefix, id, node.Name)
|
||||
for key, childNode := range node.Nodes {
|
||||
fmt.Printf("%s %s %s:\n", prefix, id, n.Name)
|
||||
for key, childNode := range n.Nodes {
|
||||
childNode.PrintNode(key, level+1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,5 @@
|
||||
package content
|
||||
|
||||
// Status status type SiteContent respnses
|
||||
type Status int
|
||||
|
||||
const (
|
||||
// StatusOk we found content
|
||||
StatusOk Status = 200
|
||||
// StatusForbidden we found content but you mst not access it
|
||||
StatusForbidden = 403
|
||||
// StatusNotFound we did not find content
|
||||
StatusNotFound = 404
|
||||
)
|
||||
|
||||
// SiteContent resolved content for a site
|
||||
type SiteContent struct {
|
||||
Status Status `json:"status"`
|
||||
|
||||
13
content/status.go
Normal file
13
content/status.go
Normal file
@ -0,0 +1,13 @@
|
||||
package content
|
||||
|
||||
// Status status type SiteContent respnses
|
||||
type Status int
|
||||
|
||||
const (
|
||||
// StatusOk we found content
|
||||
StatusOk Status = 200
|
||||
// StatusForbidden we found content but you mst not access it
|
||||
StatusForbidden = 403
|
||||
// StatusNotFound we did not find content
|
||||
StatusNotFound = 404
|
||||
)
|
||||
@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/foomo/contentserver/log"
|
||||
"github.com/foomo/contentserver/server"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDebug = "debug"
|
||||
logLevelNotice = "notice"
|
||||
logLevelWarning = "warning"
|
||||
logLevelRecord = "record"
|
||||
logLevelError = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
uniqushPushVersion = "content-server 1.3.4"
|
||||
showVersionFlag = flag.Bool("version", false, "version info")
|
||||
address = flag.String("address", "127.0.0.1:8081", "address to bind host:port")
|
||||
varDir = flag.String("var-dir", "/var/lib/contentserver", "where to put my data")
|
||||
logLevelOptions = []string{
|
||||
logLevelError,
|
||||
logLevelRecord,
|
||||
logLevelWarning,
|
||||
logLevelNotice,
|
||||
logLevelDebug,
|
||||
}
|
||||
logLevel = flag.String(
|
||||
"log-level",
|
||||
logLevelRecord,
|
||||
fmt.Sprintf(
|
||||
"one of %s",
|
||||
strings.Join(logLevelOptions, ", "),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
func exitUsage(code int) {
|
||||
fmt.Printf("Usage: %s http(s)://your-content-server/path/to/content.json\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *showVersionFlag {
|
||||
fmt.Printf("%v\n", uniqushPushVersion)
|
||||
return
|
||||
}
|
||||
if len(flag.Args()) == 1 {
|
||||
fmt.Println(*address, flag.Arg(0))
|
||||
level := log.LevelRecord
|
||||
switch *logLevel {
|
||||
case logLevelError:
|
||||
level = log.LevelError
|
||||
case logLevelRecord:
|
||||
level = log.LevelRecord
|
||||
case logLevelWarning:
|
||||
level = log.LevelWarning
|
||||
case logLevelNotice:
|
||||
level = log.LevelNotice
|
||||
case logLevelDebug:
|
||||
level = log.LevelDebug
|
||||
}
|
||||
log.SelectedLevel = level
|
||||
err := server.Run(flag.Arg(0), *address, *varDir)
|
||||
if err != nil {
|
||||
fmt.Println("exiting with error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
exitUsage(1)
|
||||
}
|
||||
}
|
||||
BIN
contentserver.graffle
Normal file
BIN
contentserver.graffle
Normal file
Binary file not shown.
327
docs/assets/Horizontal Update.svg
Normal file
327
docs/assets/Horizontal Update.svg
Normal file
@ -0,0 +1,327 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="133 101 1838 2500" width="1838" height="2500">
|
||||
<defs>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue-Bold"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_2" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="#ff2600">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_3" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="#ff2600">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<font-face font-family="Futura" font-size="80" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
</defs>
|
||||
<metadata> Produced by OmniGraffle 7.10.2
|
||||
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
|
||||
</metadata>
|
||||
<g id="Horizontal_Update" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
|
||||
<title>Horizontal Update</title>
|
||||
<rect fill="white" x="133" y="101" width="1838" height="2500"/>
|
||||
<g id="Horizontal_Update: Layer 1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_4">
|
||||
<rect x="526" y="282" width="539" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(531 303.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="209.828" y="15">*Repo.Update()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<rect x="526" y="425" width="539" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(531 446.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="200.644" y="15">*Repo.tryUpdate()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<rect x="134" y="589" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(139 610.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="23.344" y="15">if updateErr != errUpdateRejected</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_8">
|
||||
<line x1="693.9375" y1="489" x2="396.84877" y2="584.0684" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_12">
|
||||
<rect x="134" y="770" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(139 791.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="43.848" y="15">*Repo.tryToRestoreCurrent()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_13">
|
||||
<line x1="283" y1="653" x2="283" y2="756.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_15">
|
||||
<line x1="795.5" y1="346" x2="795.5" y2="411.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_16">
|
||||
<rect x="1137" y="589" width="473" height="63" fill="white"/>
|
||||
<rect x="1137" y="589" width="473" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1142 610.5)" fill="#ff2600">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="162.756" y="15">*Repo.history.lock()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_18">
|
||||
<rect x="828" y="589" width="222" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(833 610.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="15.032" y="15">return errUpdateRejected</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_20">
|
||||
<line x1="823.9375" y1="489" x2="902.0678" y2="578.29176" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_21">
|
||||
<line x1="910.0427" y1="489" x2="1246.5472" y2="584.4788" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_22">
|
||||
<rect x="824.0671" y="519.599" width="81.28906" height="32" fill="white"/>
|
||||
<text transform="translate(829.0671 524.599)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">queue full</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_23">
|
||||
<rect x="1033.0312" y="521.1068" width="93.11719" height="32" fill="white"/>
|
||||
<text transform="translate(1038.0312 526.1068)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="2.46875" y="17">queue free </tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_24">
|
||||
<rect x="134" y="966" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(139 987.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="59.832" y="15">return updateResponse</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_25">
|
||||
<line x1="283" y1="834" x2="283" y2="952.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_28">
|
||||
<rect x="982.5" y="924" width="782" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(987.5 945.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="299.496" y="16">*Repo.updateRoutine()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_29">
|
||||
<rect x="982.5" y="987" width="782" height="1072" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_31">
|
||||
<rect x="1013.5" y="1023" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1018.5 1044.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="13.305457" y="15">resChan <- *Repo.updateInProgressChannel:</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<rect x="1062.5" y="1110" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1067.5 1131.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.24146" y="15">*Repo.update()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_33">
|
||||
<rect x="1124.9145" y="1197" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1129.9145 1218.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="134.88146" y="15">*Repo.get()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_34">
|
||||
<rect x="1195.5" y="1284" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1200.5 1305.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="67.75346" y="15">*Repo.loadNodesFromJSON()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_35">
|
||||
<rect x="1259.5" y="1371" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1264.5 1392.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.47346" y="15">*Repo.loadNodes()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_36">
|
||||
<line x1="1373.5" y1="826" x2="1373.5" y2="910.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_37">
|
||||
<rect x="1302.5" y="1458" width="409.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1307.5 1461.828)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="15">for dimension, newNode := range nodes {</tspan>
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="33.448"> *Repo.updateDimension(dimension, newNode)</tspan>
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="51.895996">}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_38">
|
||||
<rect x="1352.5" y="1549" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1357.5 1570.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="47.369457" y="16">*Repo.dimensionUpdateRoutine()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_39">
|
||||
<rect x="1352.5" y="1612" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1357.5 1633.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="79.44946" y="15">*Repo._updateDimension()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_40">
|
||||
<rect x="1352.5" y="1675" width="359.1709" height="343" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_41">
|
||||
<rect x="1373.5" y="1689" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1378.5 1710.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="60.152" y="15">newNode.WireParents()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_42">
|
||||
<rect x="1373.5" y="1772.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1378.5 1794)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="90.072" y="15">buildDirectory()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_43">
|
||||
<rect x="1373.5" y="1856" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1378.5 1877.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="99.872" y="15">wireAliases()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_44">
|
||||
<rect x="1373.5" y="1939.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1378.5 1961)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="20.8" y="15">dimensionUpdateDoneChan <- err</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_45">
|
||||
<rect x="982.5" y="2186" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(987.5 2207.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="71.76146" y="15">*Repo.history.add(jsonBytes)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_46">
|
||||
<line x1="1210.0304" y1="2060" x2="1175.7356" y2="2172.6591" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_48">
|
||||
<rect x="982.5" y="2537" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(987.5 2558.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="62.79346" y="15">resultChan <- updateResponse</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_47">
|
||||
<line x1="1162.0855" y1="2488" x2="1162.0855" y2="2523.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_49">
|
||||
<line x1="1265.4012" y1="1174" x2="1273.6646" y2="1185.5183" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_50">
|
||||
<line x1="1330.8681" y1="1261" x2="1340.5898" y2="1272.9824" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_51">
|
||||
<line x1="1398.9935" y1="1348" x2="1407.5333" y2="1359.6088" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_52">
|
||||
<line x1="1464.4878" y1="1435" x2="1473.739" y2="1446.8363" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_53">
|
||||
<line x1="1522.5" y1="1753" x2="1522.5" y2="1758.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_54">
|
||||
<line x1="1522.5" y1="1836.5" x2="1522.5" y2="1842.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_55">
|
||||
<line x1="1522.5" y1="1920" x2="1522.5" y2="1925.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_57">
|
||||
<rect x="485.6393" y="523.2829" width="102.32812" height="32" fill="white"/>
|
||||
<text transform="translate(490.6393 528.2829)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">update error</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_60">
|
||||
<rect x="519" y="589" width="222" height="63" fill="white"/>
|
||||
<rect x="519" y="589" width="222" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(524 610.5)" fill="#ff2600">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="15.032" y="15">return errUpdateRejected</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_59">
|
||||
<line x1="762.7027" y1="489" x2="671.9604" y2="578.91994" marker-end="url(#FilledArrow_Marker_2)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_58">
|
||||
<rect x="663.3299" y="519.599" width="104.69531" height="32" fill="white"/>
|
||||
<text transform="translate(668.3299 524.599)" fill="#ff2600">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="#ff2600" x="0" y="17">lockfile exists</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_61">
|
||||
<rect x="1137" y="762" width="473" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1142 783.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="8.42" y="15">*Repo.updateInProgressChan <- make(chan updateResponse)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_62">
|
||||
<line x1="1373.5" y1="653" x2="1373.5" y2="748.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_63">
|
||||
<rect x="982.5" y="2305" width="359.1709" height="63" fill="white"/>
|
||||
<rect x="982.5" y="2305" width="359.1709" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(987.5 2326.5)" fill="#ff2600">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="96.94546" y="15">*Repo.history.unlock()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_64">
|
||||
<line x1="1162.0855" y1="2250" x2="1162.0855" y2="2291.1" marker-end="url(#FilledArrow_Marker_2)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_66">
|
||||
<rect x="982.5" y="2424" width="359.1709" height="63" fill="white"/>
|
||||
<rect x="982.5" y="2424" width="359.1709" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(987.5 2445.5)" fill="#ff2600">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="58.27346" y="15">*Repo.history.</tspan>
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" y="15">broadcastUpdate()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_67">
|
||||
<line x1="1162.0855" y1="2369" x2="1162.0855" y2="2410.1" marker-end="url(#FilledArrow_Marker_3)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_68">
|
||||
<text transform="translate(139 106.61719)" fill="black">
|
||||
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" x="0" y="83">Contentserver Horizontal Scaling: Update Flow</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_69">
|
||||
<path d="M 1341.6709 2305 L 1700.8418 2305 L 1700.8418 2368 L 1341.6709 2368 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
|
||||
<text transform="translate(1346.6709 2326.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.84146" y="15">Remove lockfile</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_70">
|
||||
<path d="M 1341.6709 2424 L 1700.8418 2424 L 1700.8418 2487 L 1341.6709 2487 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
|
||||
<text transform="translate(1346.6709 2445.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="76.04946" y="15">Broadcast update via NATS</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_71">
|
||||
<path d="M 1610 589 L 1969.171 589 L 1969.171 652 L 1610 652 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
|
||||
<text transform="translate(1615 610.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="123.62546" y="15">Create lockfile</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
391
docs/assets/Horizontal.svg
Normal file
391
docs/assets/Horizontal.svg
Normal file
@ -0,0 +1,391 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-263 -474 2433 1740" width="2433" height="1740">
|
||||
<defs>
|
||||
<font-face font-family="Helvetica Neue" font-size="25" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue-Bold"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
</defs>
|
||||
<metadata> Produced by OmniGraffle 7.10.2
|
||||
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
|
||||
</metadata>
|
||||
<g id="Horizontal" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
|
||||
<title>Horizontal</title>
|
||||
<rect fill="white" x="-263" y="-474" width="2433" height="1740"/>
|
||||
<g id="Horizontal: Layer 1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_35">
|
||||
<rect x="-65" y="991" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<rect x="775" y="984" width="554" height="231" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_20">
|
||||
<rect x="-65" y="195" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_36">
|
||||
<rect x="-262" y="342" width="384" height="408" fill="white"/>
|
||||
<rect x="-262" y="342" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_21">
|
||||
<rect x="465" y="361.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_2">
|
||||
<rect x="-65" y="121.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-60 142.75)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="390.1" y="24">contentserver</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_3">
|
||||
<rect x="140.5" y="425.4524" width="306" height="84" fill="white"/>
|
||||
<rect x="140.5" y="425.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(145.5 457.4524)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_4">
|
||||
<rect x="140.5" y="567.5476" width="306" height="84" fill="white"/>
|
||||
<rect x="140.5" y="567.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(145.5 599.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<rect x="886" y="-368.5" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(891 -336.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="87.992" y="15">CONTENT.JSON</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_6">
|
||||
<rect x="847" y="-473" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(852 -455)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="21.475" y="24">Content Source Webservice</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<rect x="465" y="285" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(470 307.9499)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="155.525" y="24">Repo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_9">
|
||||
<rect x="670" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 430.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_11">
|
||||
<rect x="495" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 430.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_22">
|
||||
<rect x="670" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 543.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_23">
|
||||
<rect x="495" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 543.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_24">
|
||||
<rect x="847" y="-406" width="384" height="162.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_25">
|
||||
<rect x="-223" y="446.06663" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="446.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 463.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_26">
|
||||
<rect x="-223" y="379" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="379" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 396.48096)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_27">
|
||||
<rect x="-223" y="513.13327" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="513.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 530.6142)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_28">
|
||||
<rect x="-223" y="580.1999" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="580.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 597.68086)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_29">
|
||||
<rect x="-223" y="650.5381" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="650.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 668.019)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_30">
|
||||
<rect x="831.2656" y="1071" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(836.2656 1084)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="102.52637" y="15">contentserver-repo-current.json</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_31">
|
||||
<rect x="775" y="917" width="554" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(780 935)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="115.975" y="24">Var Directory (aka History)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_34">
|
||||
<rect x="-65" y="924" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-60 942)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="36.7875" y="24">Logfile (JSON or TXT)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_37">
|
||||
<rect x="-262" y="275" width="384" height="67" fill="white"/>
|
||||
<rect x="-262" y="275" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-257 293)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="166.4125" y="24">API</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_39">
|
||||
<rect x="670" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 657.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_40">
|
||||
<rect x="495" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 657.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_43">
|
||||
<rect x="831.2656" y="1011.5" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(836.2656 1024.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="97.70237" y="15">contentserver-repo-[Timestamp].json</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_45">
|
||||
<rect x="-42" y="1011.5" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-37 1023.5)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_44">
|
||||
<rect x="-42" y="1071" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-37 1083)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_46">
|
||||
<rect x="-42" y="1130.5" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-37 1142.5)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_47">
|
||||
<rect x="-42" y="1190" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-37 1202)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_48">
|
||||
<line x1="461.9265" y1="120.5" x2="922.1852" y2="-234.61976" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_56">
|
||||
<line x1="202.23013" y1="815" x2="136.16397" y2="912.3267" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_57">
|
||||
<line x1="857.8643" y1="815" x2="991.9924" y2="908.6168" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_59">
|
||||
<rect x="1213" y="187" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_60">
|
||||
<rect x="1016" y="334" width="384" height="408" fill="white"/>
|
||||
<rect x="1016" y="334" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_61">
|
||||
<rect x="1743" y="353.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_62">
|
||||
<rect x="1213" y="113.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1218 134.75)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="390.1" y="24">contentserver</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_63">
|
||||
<rect x="1418.5" y="417.4524" width="306" height="84" fill="white"/>
|
||||
<rect x="1418.5" y="417.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1423.5 449.4524)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_64">
|
||||
<rect x="1418.5" y="559.5476" width="306" height="84" fill="white"/>
|
||||
<rect x="1418.5" y="559.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1423.5 591.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_65">
|
||||
<rect x="1743" y="277" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1748 299.9499)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="155.525" y="24">Repo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_66">
|
||||
<rect x="1948" y="388.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1953 422.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_67">
|
||||
<rect x="1773" y="388.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1778 422.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_68">
|
||||
<rect x="1948" y="501.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1953 535.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_69">
|
||||
<rect x="1773" y="501.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1778 535.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_70">
|
||||
<rect x="1055" y="438.06663" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="1055" y="438.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1060 455.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_71">
|
||||
<rect x="1055" y="371" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="1055" y="371" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1060 388.48096)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_72">
|
||||
<rect x="1055" y="505.13327" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="1055" y="505.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1060 522.6142)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_73">
|
||||
<rect x="1055" y="572.1999" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="1055" y="572.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1060 589.68086)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_74">
|
||||
<rect x="1055" y="642.5381" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="1055" y="642.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1060 660.019)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_75">
|
||||
<rect x="1016" y="267" width="384" height="67" fill="white"/>
|
||||
<rect x="1016" y="267" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1021 285)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="166.4125" y="24">API</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_76">
|
||||
<rect x="1948" y="615.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1953 649.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_77">
|
||||
<rect x="1773" y="615.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1778 649.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_78">
|
||||
<line x1="1903.2618" y1="807" x2="1967.6353" y2="901.1666" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_79">
|
||||
<line x1="1253.9747" y1="807" x2="1111.0744" y2="908.5285" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_84">
|
||||
<line x1="1639.1832" y1="112.5" x2="1162.3254" y2="-234.90403" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_85">
|
||||
<rect x="1828" y="979.816" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_86">
|
||||
<rect x="1828" y="912.816" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1833 930.816)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="36.7875" y="24">Logfile (JSON or TXT)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_87">
|
||||
<rect x="1851" y="1000.316" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1856 1012.316)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_88">
|
||||
<rect x="1851" y="1059.816" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1856 1071.816)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_89">
|
||||
<rect x="1851" y="1119.316" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1856 1131.316)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_90">
|
||||
<rect x="1851" y="1178.816" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1856 1190.816)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_91">
|
||||
<rect x="831.2656" y="1137" width="441.46875" height="46" fill="#ccc"/>
|
||||
<rect x="831.2656" y="1137" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(836.2656 1150)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="135.42237" y="15">updateInProgress.lock</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 25 KiB |
250
docs/assets/Overview.svg
Normal file
250
docs/assets/Overview.svg
Normal file
@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-549 -177 1441 1431" width="1441" height="1431">
|
||||
<defs>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue-Bold"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
</defs>
|
||||
<metadata> Produced by OmniGraffle 7.10.2
|
||||
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
|
||||
</metadata>
|
||||
<g id="Overview" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
|
||||
<title>Overview</title>
|
||||
<rect fill="white" x="-549" y="-177" width="1441" height="1431"/>
|
||||
<g id="Overview: Layer 1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_35">
|
||||
<rect x="546" y="979.7753" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<rect x="-69" y="979.816" width="554" height="185.45933" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_20">
|
||||
<rect x="-65" y="195" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_36">
|
||||
<rect x="-262" y="342" width="384" height="408" fill="white"/>
|
||||
<rect x="-262" y="342" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_21">
|
||||
<rect x="465" y="361.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_2">
|
||||
<rect x="-65" y="121.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-60 148.25)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="419.944" y="16">contentserver</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_3">
|
||||
<rect x="140.5" y="425.4524" width="306" height="84" fill="white"/>
|
||||
<rect x="140.5" y="425.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(145.5 457.4524)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_4">
|
||||
<rect x="140.5" y="567.5476" width="306" height="84" fill="white"/>
|
||||
<rect x="140.5" y="567.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(145.5 599.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<rect x="260" y="-71.5" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(265 -39.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="87.992" y="15">CONTENT.JSON</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_6">
|
||||
<rect x="221" y="-176" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(226 -152.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="81.064" y="16">Content Source Webservice</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<rect x="465" y="285" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(470 313.4499)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="166.856" y="16">Repo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_9">
|
||||
<rect x="670" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 430.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_11">
|
||||
<rect x="495" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 430.89207)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_22">
|
||||
<rect x="670" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 543.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_23">
|
||||
<rect x="495" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 543.88767)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_24">
|
||||
<rect x="221" y="-109" width="384" height="162.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_25">
|
||||
<rect x="-223" y="446.06663" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="446.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 463.5476)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_26">
|
||||
<rect x="-223" y="379" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="379" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 396.48096)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_27">
|
||||
<rect x="-223" y="513.13327" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="513.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 530.6142)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_28">
|
||||
<rect x="-223" y="580.1999" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="580.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 597.68086)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_29">
|
||||
<rect x="-223" y="650.5381" width="306" height="54.961924" fill="white"/>
|
||||
<rect x="-223" y="650.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-218 668.019)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_30">
|
||||
<rect x="-12.734375" y="1088.724" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-7.734375 1101.724)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="102.52637" y="15">contentserver-repo-current.json</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_31">
|
||||
<rect x="-69" y="912.816" width="554" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-64 936.316)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="172.144" y="16">Var Directory (aka History)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_34">
|
||||
<rect x="546" y="912.7753" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(551 936.2753)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="83.124" y="16">Logfile (JSON or TXT)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_37">
|
||||
<rect x="-262" y="275" width="384" height="67" fill="white"/>
|
||||
<rect x="-262" y="275" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-257 298.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="173.824" y="16">API</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_39">
|
||||
<rect x="670" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(675 657.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_40">
|
||||
<rect x="495" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(500 657.0526)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15">…</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_41">
|
||||
<rect x="-12.734375" y="1010.3673" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-7.734375 1023.3673)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="97.70237" y="15">contentserver-repo-[Timestamp].json</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_45">
|
||||
<rect x="569" y="1000.2753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(574 1012.2753)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_44">
|
||||
<rect x="569" y="1059.7753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(574 1071.7753)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_46">
|
||||
<rect x="569" y="1119.2753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(574 1131.2753)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_47">
|
||||
<rect x="569" y="1178.7753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(574 1190.7753)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_48">
|
||||
<line x1="413" y1="120.5" x2="413" y2="67.4" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_49">
|
||||
<line x1="-469.47585" y1="537.66146" x2="-237.4888" y2="419.19996" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_50">
|
||||
<line x1="-469.47585" y1="537.66146" x2="-238.52136" y2="480.43586" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_51">
|
||||
<line x1="-469.47585" y1="537.66146" x2="-236.89965" y2="539.3806" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_53">
|
||||
<line x1="-464.51305" y1="540.5356" x2="-238.39468" y2="605.75816" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_52">
|
||||
<line x1="-469.47585" y1="537.66146" x2="-237.1631" y2="672.2017" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_56">
|
||||
<line x1="626.31374" y1="815" x2="685.49386" y2="901.1427" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_57">
|
||||
<line x1="268.92984" y1="815" x2="229.43732" y2="900.1143" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_58">
|
||||
<rect x="-548" y="500.94756" width="119.31591" height="84" fill="white"/>
|
||||
<rect x="-548" y="500.94756" width="119.31591" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(-543 532.94756)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.065957" y="15">Client</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
260
docs/assets/Update-Flow.svg
Normal file
260
docs/assets/Update-Flow.svg
Normal file
@ -0,0 +1,260 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="314 98 1205 2185" width="1205" height="2185">
|
||||
<defs>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
|
||||
<g>
|
||||
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
|
||||
</g>
|
||||
</marker>
|
||||
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
|
||||
<font-face-src>
|
||||
<font-face-name name="HelveticaNeue-Bold"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
<font-face font-family="Futura" font-size="80" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
|
||||
<font-face-src>
|
||||
<font-face-name name="Futura-Medium"/>
|
||||
</font-face-src>
|
||||
</font-face>
|
||||
</defs>
|
||||
<metadata> Produced by OmniGraffle 7.10.2
|
||||
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
|
||||
</metadata>
|
||||
<g id="Update-Flow" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
|
||||
<title>Update-Flow</title>
|
||||
<rect fill="white" x="314" y="98" width="1205" height="2185"/>
|
||||
<g id="Update-Flow: Layer 1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_4">
|
||||
<rect x="526" y="282" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(531 303.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="89.328" y="15">*Repo.Update()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<rect x="526" y="425" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(531 446.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="80.144" y="15">*Repo.tryUpdate()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<rect x="315" y="587" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(320 608.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="23.344" y="15">if updateErr != errUpdateRejected</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_8">
|
||||
<line x1="632.66975" y1="489" x2="516.5623" y2="578.1441" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_10">
|
||||
<rect x="545.27846" y="518.599" width="56" height="32" fill="white"/>
|
||||
<text transform="translate(550.27846 523.599)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">failure</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_12">
|
||||
<rect x="315" y="768" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(320 789.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="43.848" y="15">*Repo.tryToRestoreCurrent()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_13">
|
||||
<line x1="464" y1="651" x2="464" y2="754.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_15">
|
||||
<line x1="675" y1="346" x2="675" y2="411.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_16">
|
||||
<rect x="1045" y="768" width="473" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1050 789.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="8.42" y="15">*Repo.updateInProgressChan <- make(chan updateResponse)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_18">
|
||||
<rect x="736" y="768" width="222" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(741 789.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="15.032" y="15">return errUpdateRejected</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_19">
|
||||
<rect x="522.1144" y="518.599" width="102.32812" height="32" fill="white"/>
|
||||
<text transform="translate(527.1144 523.599)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">update error</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_20">
|
||||
<line x1="691.2974" y1="489" x2="824.9201" y2="755.4686" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_21">
|
||||
<line x1="732.4672" y1="489" x2="1212.8041" y2="760.6497" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_22">
|
||||
<rect x="716.2705" y="603.8538" width="81.28906" height="32" fill="white"/>
|
||||
<text transform="translate(721.2705 608.8538)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">queue full</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_23">
|
||||
<rect x="924.7738" y="608.0878" width="93.11719" height="32" fill="white"/>
|
||||
<text transform="translate(929.7738 613.0878)" fill="black">
|
||||
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="2.46875" y="17">queue free </tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_24">
|
||||
<rect x="315" y="964" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(320 985.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="59.832" y="15">return updateResponse</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_25">
|
||||
<line x1="464" y1="832" x2="464" y2="950.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_28">
|
||||
<rect x="736" y="883" width="782" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(741 904.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="299.496" y="16">*Repo.updateRoutine()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_29">
|
||||
<rect x="736" y="946" width="782" height="1072" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_31">
|
||||
<rect x="767" y="982" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(772 1003.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="13.305457" y="15">resChan <- *Repo.updateInProgressChannel:</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<rect x="816" y="1069" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(821 1090.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.24146" y="15">*Repo.update()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_33">
|
||||
<rect x="878.4145" y="1156" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(883.4145 1177.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="134.88146" y="15">*Repo.get()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_34">
|
||||
<rect x="949" y="1243" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(954 1264.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="67.75346" y="15">*Repo.loadNodesFromJSON()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_35">
|
||||
<rect x="1013" y="1330" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1018 1351.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.47346" y="15">*Repo.loadNodes()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_36">
|
||||
<line x1="1237.837" y1="832" x2="1181.0111" y2="874.2976" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_37">
|
||||
<rect x="1056" y="1417" width="409.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1061 1420.828)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="15">for dimension, newNode := range nodes {</tspan>
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="33.448"> *Repo.updateDimension(dimension, newNode)</tspan>
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="51.895996">}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_38">
|
||||
<rect x="1106" y="1508" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1111 1529.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="47.369457" y="16">*Repo.dimensionUpdateRoutine()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_39">
|
||||
<rect x="1106" y="1571" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1111 1592.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="79.44946" y="15">*Repo._updateDimension()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_40">
|
||||
<rect x="1106" y="1634" width="359.1709" height="343" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_41">
|
||||
<rect x="1127" y="1648" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1132 1669.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="60.152" y="15">newNode.WireParents()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_42">
|
||||
<rect x="1127" y="1731.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1132 1753)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="90.072" y="15">buildDirectory()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_43">
|
||||
<rect x="1127" y="1815" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1132 1836.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="99.872" y="15">wireAliases()</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_44">
|
||||
<rect x="1127" y="1898.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(1132 1920)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="20.8" y="15">dimensionUpdateDoneChan <- err</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_45">
|
||||
<rect x="736" y="2078" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(741 2099.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="71.76146" y="15">*Repo.history.add(jsonBytes)</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_46">
|
||||
<line x1="946.0763" y1="2019" x2="930.6539" y2="2064.7752" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_48">
|
||||
<rect x="766.5855" y="2219" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
<text transform="translate(771.5855 2240.5)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="32.208" y="15">resultChan <- updateResponse</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Line_47">
|
||||
<line x1="915.5855" y1="2142" x2="915.5855" y2="2205.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_49">
|
||||
<line x1="1018.9012" y1="1133" x2="1027.1646" y2="1144.5183" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_50">
|
||||
<line x1="1084.3681" y1="1220" x2="1094.0898" y2="1231.9824" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_51">
|
||||
<line x1="1152.4935" y1="1307" x2="1161.0333" y2="1318.6088" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_52">
|
||||
<line x1="1217.9878" y1="1394" x2="1227.2391" y2="1405.8363" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_53">
|
||||
<line x1="1276" y1="1712" x2="1276" y2="1717.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_54">
|
||||
<line x1="1276" y1="1795.5" x2="1276" y2="1801.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Line_55">
|
||||
<line x1="1276" y1="1879" x2="1276" y2="1884.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Graphic_57">
|
||||
<text transform="translate(411 103)" fill="black">
|
||||
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" x="0" y="83">Contentserver</tspan>
|
||||
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" y="83">:</tspan>
|
||||
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" y="83"> Update Flow</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
136
go.mod
Normal file
136
go.mod
Normal file
@ -0,0 +1,136 @@
|
||||
module github.com/foomo/contentserver
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/foomo/keel v0.20.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/sync v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.121.1 // indirect
|
||||
cloud.google.com/go/auth v0.16.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/firestore v1.18.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/avast/retry-go/v4 v4.6.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/ebitengine/purego v0.8.3 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fbiville/markdown-table-formatter v0.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/foomo/gostandards v0.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/hashicorp/consul/api v1.32.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-metrics v0.5.4 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/serf v0.10.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nats.go v1.42.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/sagikazarmark/crypt v0.28.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.4 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/spf13/viper/remote v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.21 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/api v0.233.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
731
go.sum
Normal file
731
go.sum
Normal file
@ -0,0 +1,731 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
|
||||
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
|
||||
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
|
||||
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute v1.37.0 h1:XxtZlXYkZXub3LNaLu90TTemcFqIU1yZ4E4q9VlR39A=
|
||||
cloud.google.com/go/compute v1.37.0/go.mod h1:AsK4VqrSyXBo4SMbRtfAO1VfaMjUEjEwv1UB/AwVp5Q=
|
||||
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
|
||||
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
|
||||
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
|
||||
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
|
||||
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
|
||||
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
|
||||
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
|
||||
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
|
||||
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
|
||||
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNYvwXQMMMIKNZop2SSho=
|
||||
github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/foomo/gostandards v0.2.0 h1:Ryd7TI9yV3Xk5B84DcUDB7KcL3LzQ8NS+TVOrFxTYfA=
|
||||
github.com/foomo/gostandards v0.2.0/go.mod h1:XQx7Ur6vyvxaIe2cQvAthuhPYDe+d2soibqVcXDXOh4=
|
||||
github.com/foomo/keel v0.19.0 h1:8uIinFat9Jj72zyWx6c+30f2o0EdXZ350s/caEC37P8=
|
||||
github.com/foomo/keel v0.19.0/go.mod h1:eyO1lVDIvuIOFjWdIx5MqnWmk0E0FZWZwFhtLiVkTio=
|
||||
github.com/foomo/keel v0.20.0 h1:kgVPKIdls2hzbuEmD2BTKeCGyV+H38d4Z4Z9ihNKOy0=
|
||||
github.com/foomo/keel v0.20.0/go.mod h1:AefbM40PS2EJpwZn4EXtQbzbWDZlbYH0N2DB/zQ0YE8=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8=
|
||||
github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE=
|
||||
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||
github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8=
|
||||
github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
|
||||
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
|
||||
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
|
||||
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
|
||||
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E=
|
||||
github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78=
|
||||
github.com/sagikazarmark/crypt v0.28.0 h1:g5V74hutj/d3fn5Ga3/3GxUjg1k9H0NfSDjDUcBNpIs=
|
||||
github.com/sagikazarmark/crypt v0.28.0/go.mod h1:stOy168PraSkc5DJisfihVxPnsXUAVIcIzy/MQh49DA=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM=
|
||||
github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper/remote v1.20.1 h1:0qVzx4wHqc62HOJDCc/7tcvjLmHjUf4KFQE3RBXfC3k=
|
||||
github.com/spf13/viper/remote v1.20.1/go.mod h1:Q1UYWvOAkwFm9ntDssWgf1L07rMj1cZ5BerO2gBa6zg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU=
|
||||
github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c=
|
||||
go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
|
||||
go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM=
|
||||
go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA=
|
||||
go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI=
|
||||
go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E=
|
||||
go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA=
|
||||
go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8=
|
||||
go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw=
|
||||
go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk=
|
||||
go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik=
|
||||
go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE=
|
||||
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 h1:LD6TMRg2hfNzkMD36Pq0jeYBcSP9W0aJt41Zmje43Ig=
|
||||
go.opentelemetry.io/contrib/instrumentation/host v0.60.0/go.mod h1:GN4xnih1u2OQeRs8rNJ13XR8XsTqFopc57e/3Kf0h6c=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
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/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
|
||||
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU=
|
||||
google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o=
|
||||
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
|
||||
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
|
||||
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
|
||||
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 h1:0DnDgelxbooHLt0nyiPeCP0zrH/RL+UG558i1oNU1xE=
|
||||
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:IuQRZAKkz+Mhos3ZZ0+hcGaTmLuuTuGw344uzwztGl8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
79
log/log.go
79
log/log.go
@ -1,79 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Level logging level enum
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// LevelError an error - as bad as it gets
|
||||
LevelError Level = 0
|
||||
// LevelRecord put this to the logs in any case
|
||||
LevelRecord Level = 1
|
||||
// LevelWarning not that bad
|
||||
LevelWarning Level = 2
|
||||
// LevelNotice almost on debug level
|
||||
LevelNotice Level = 3
|
||||
// LevelDebug we are debugging
|
||||
LevelDebug Level = 4
|
||||
)
|
||||
|
||||
// SelectedLevel selected log level
|
||||
var SelectedLevel = LevelDebug
|
||||
|
||||
var prefices = map[Level]string{
|
||||
LevelRecord: "record : ",
|
||||
LevelError: "error : ",
|
||||
LevelWarning: "warning : ",
|
||||
LevelNotice: "notice : ",
|
||||
LevelDebug: "debug : ",
|
||||
}
|
||||
|
||||
func log(msg string, level Level) string {
|
||||
if level <= SelectedLevel {
|
||||
prefix := time.Now().Format(time.RFC3339Nano) + " " + prefices[level]
|
||||
lines := strings.Split(msg, "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
fmt.Println(prefix + lines[i])
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func logThings(msgs []interface{}, level Level) string {
|
||||
r := ""
|
||||
for _, msg := range msgs {
|
||||
r += "\n" + fmt.Sprint(msg)
|
||||
}
|
||||
r = strings.Trim(r, "\n")
|
||||
return log(r, level)
|
||||
}
|
||||
|
||||
// Debug write debug messages to the log
|
||||
func Debug(msgs ...interface{}) string {
|
||||
return logThings(msgs, LevelDebug)
|
||||
}
|
||||
|
||||
// Notice write notice messages to the log
|
||||
func Notice(msgs ...interface{}) string {
|
||||
return logThings(msgs, LevelNotice)
|
||||
}
|
||||
|
||||
// Warning write warning messages to the log
|
||||
func Warning(msgs ...interface{}) string {
|
||||
return logThings(msgs, LevelWarning)
|
||||
}
|
||||
|
||||
// Record write record messages to the log
|
||||
func Record(msgs ...interface{}) string {
|
||||
return logThings(msgs, LevelRecord)
|
||||
}
|
||||
|
||||
// Error write error messages to the log
|
||||
func Error(msgs ...interface{}) string {
|
||||
return logThings(msgs, LevelError)
|
||||
}
|
||||
9
main.go
Normal file
9
main.go
Normal file
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/foomo/contentserver/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
Packaging & Deployment
|
||||
----------------------
|
||||
|
||||
In order to build packages and upload to Package Cloud, please install the following requirements and run the make task.
|
||||
|
||||
[Package Cloud Command Line Client](https://packagecloud.io/docs#cli_install)
|
||||
|
||||
```
|
||||
$ gem install package_cloud
|
||||
```
|
||||
|
||||
[FPM](https://github.com/jordansissel/fpm)
|
||||
|
||||
```
|
||||
$ gem install fpm
|
||||
```
|
||||
|
||||
Building package
|
||||
|
||||
```
|
||||
$ make package
|
||||
```
|
||||
|
||||
*NOTE: you will be prompted for Package Cloud credentials.*
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
```
|
||||
$ git clone https://github.com/foomo/contentserver.git
|
||||
$ cd contentserver
|
||||
$ make test
|
||||
```
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests and examples for any new or changed functionality.
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`\)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`\)
|
||||
4. Push to the branch (`git push origin my-new-feature`\)
|
||||
5. Create new Pull Request
|
||||
61
pkg/build.sh
61
pkg/build.sh
@ -1,61 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
USER="foomo"
|
||||
NAME="content-server"
|
||||
URL="http://www.foomo.org"
|
||||
DESCRIPTION="Serves content tree structures very quickly through a json socket api."
|
||||
LICENSE="LGPL-3.0"
|
||||
|
||||
# get version
|
||||
VERSION=`bin/content-server --version | sed 's/content-server //'`
|
||||
|
||||
# create temp dir
|
||||
TEMP=`pwd`/pkg/tmp
|
||||
mkdir -p $TEMP
|
||||
|
||||
package()
|
||||
{
|
||||
OS=$1
|
||||
ARCH=$2
|
||||
TYPE=$3
|
||||
TARGET=$4
|
||||
|
||||
# copy license file
|
||||
cp LICENSE $LICENSE
|
||||
|
||||
# define source dir
|
||||
SOURCE=`pwd`/pkg/${TYPE}
|
||||
|
||||
# create build folder
|
||||
BUILD=${TEMP}/${NAME}-${VERSION}
|
||||
#rsync -rv --exclude **/.git* --exclude /*.sh $SOURCE/ $BUILD/
|
||||
|
||||
# build binary
|
||||
GOOS=$OS GOARCH=$ARCH go build -o $BUILD/usr/local/bin/${NAME}
|
||||
|
||||
# create package
|
||||
fpm -s dir \
|
||||
-t $TYPE \
|
||||
--name $NAME \
|
||||
--maintainer $USER \
|
||||
--version $VERSION \
|
||||
--license $LICENSE \
|
||||
--description "${DESCRIPTION}" \
|
||||
--architecture $ARCH \
|
||||
--package $TEMP \
|
||||
--url "${URL}" \
|
||||
-C $BUILD \
|
||||
.
|
||||
|
||||
# push
|
||||
package_cloud push $TARGET $TEMP/${NAME}_${VERSION}_${ARCH}.${TYPE}
|
||||
|
||||
# cleanup
|
||||
rm -rf $TEMP
|
||||
rm $LICENSE
|
||||
}
|
||||
|
||||
package linux amd64 deb foomo/content-server/ubuntu/precise
|
||||
package linux amd64 deb foomo/content-server/ubuntu/trusty
|
||||
|
||||
#package linux amd64 rpm
|
||||
175
pkg/handler/http.go
Normal file
175
pkg/handler/http.go
Normal file
@ -0,0 +1,175 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/metrics"
|
||||
"github.com/foomo/contentserver/pkg/repo"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
httputils "github.com/foomo/keel/utils/net/http"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
HTTP struct {
|
||||
l *zap.Logger
|
||||
repo *repo.Repo
|
||||
basePath string
|
||||
}
|
||||
HTTPOption func(*HTTP)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// NewHTTP returns a shiny new web server
|
||||
func NewHTTP(l *zap.Logger, repo *repo.Repo, opts ...HTTPOption) http.Handler {
|
||||
inst := &HTTP{
|
||||
l: l.Named("http"),
|
||||
basePath: "/contentserver",
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(inst)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Options
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func WithBasePath(v string) HTTPOption {
|
||||
return func(o *HTTP) {
|
||||
o.basePath = v
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *HTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
httputils.ServerError(h.l, w, r, http.StatusMethodNotAllowed, errors.New("method not allowed"))
|
||||
return
|
||||
}
|
||||
if r.Body == nil {
|
||||
httputils.BadRequestServerError(h.l, w, r, errors.New("empty request body"))
|
||||
return
|
||||
}
|
||||
|
||||
bytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
httputils.BadRequestServerError(h.l, w, r, errors.Wrap(err, "failed to read incoming request"))
|
||||
return
|
||||
}
|
||||
|
||||
route := Route(strings.TrimPrefix(r.URL.Path, h.basePath+"/"))
|
||||
if route == RouteGetRepo {
|
||||
h.repo.WriteRepoBytes(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return
|
||||
}
|
||||
|
||||
reply, errReply := h.handleRequest(h.repo, route, bytes, "webserver")
|
||||
if errReply != nil {
|
||||
http.Error(w, errReply.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(reply)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Private methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *HTTP) handleRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) ([]byte, error) {
|
||||
start := time.Now()
|
||||
|
||||
reply, err := h.executeRequest(r, route, jsonBytes, source)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "error"
|
||||
}
|
||||
|
||||
metrics.ServiceRequestCounter.WithLabelValues(string(route), result, source).Inc()
|
||||
metrics.ServiceRequestDuration.WithLabelValues(string(route), result, source).Observe(time.Since(start).Seconds())
|
||||
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (h *HTTP) executeRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) (replyBytes []byte, err error) {
|
||||
var (
|
||||
reply interface{}
|
||||
apiErr error
|
||||
jsonErr error
|
||||
processIfJSONIsOk = func(err error, processingFunc func()) {
|
||||
if err != nil {
|
||||
jsonErr = err
|
||||
return
|
||||
}
|
||||
processingFunc()
|
||||
}
|
||||
)
|
||||
metrics.ContentRequestCounter.WithLabelValues(source).Inc()
|
||||
|
||||
// handle and process
|
||||
switch route {
|
||||
// case HandlerGetRepo: // This case is handled prior to handleRequest being called.
|
||||
// since the resulting bytes are written directly in to the http.ResponseWriter / net.Connection
|
||||
case RouteGetURIs:
|
||||
getURIRequest := &requests.URIs{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &getURIRequest), func() {
|
||||
reply = r.GetURIs(getURIRequest.Dimension, getURIRequest.IDs)
|
||||
})
|
||||
case RouteGetContent:
|
||||
contentRequest := &requests.Content{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &contentRequest), func() {
|
||||
reply, apiErr = r.GetContent(contentRequest)
|
||||
})
|
||||
case RouteGetNodes:
|
||||
nodesRequest := &requests.Nodes{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &nodesRequest), func() {
|
||||
reply = r.GetNodes(nodesRequest)
|
||||
})
|
||||
case RouteUpdate:
|
||||
updateRequest := &requests.Update{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &updateRequest), func() {
|
||||
reply = r.Update()
|
||||
})
|
||||
default:
|
||||
reply = responses.NewError(1, "unknown route: "+string(route))
|
||||
}
|
||||
|
||||
// error handling
|
||||
if jsonErr != nil {
|
||||
h.l.Error("could not read incoming json", zap.Error(jsonErr))
|
||||
reply = responses.NewError(2, "could not read incoming json "+jsonErr.Error())
|
||||
} else if apiErr != nil {
|
||||
h.l.Error("an API error occurred", zap.Error(apiErr))
|
||||
reply = responses.NewError(3, "internal error "+apiErr.Error())
|
||||
}
|
||||
|
||||
return h.encodeReply(reply)
|
||||
}
|
||||
|
||||
// encodeReply takes an interface and encodes it as JSON
|
||||
// it returns the resulting JSON and a marshalling error
|
||||
func (h *HTTP) encodeReply(reply interface{}) (bytes []byte, err error) {
|
||||
bytes, err = json.Marshal(map[string]interface{}{
|
||||
"reply": reply,
|
||||
})
|
||||
if err != nil {
|
||||
h.l.Error("could not encode reply", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
7
pkg/handler/json.go
Normal file
7
pkg/handler/json.go
Normal file
@ -0,0 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
17
pkg/handler/route.go
Normal file
17
pkg/handler/route.go
Normal file
@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
// Route type
|
||||
type Route string
|
||||
|
||||
const (
|
||||
// RouteGetURIs get uris, many at once, to keep it fast
|
||||
RouteGetURIs Route = "getURIs"
|
||||
// RouteGetContent get (site) content
|
||||
RouteGetContent Route = "getContent"
|
||||
// RouteGetNodes get nodes
|
||||
RouteGetNodes Route = "getNodes"
|
||||
// RouteUpdate update repo
|
||||
RouteUpdate Route = "update"
|
||||
// RouteGetRepo get the whole repo
|
||||
RouteGetRepo Route = "getRepo"
|
||||
)
|
||||
273
pkg/handler/socket.go
Normal file
273
pkg/handler/socket.go
Normal file
@ -0,0 +1,273 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/metrics"
|
||||
"github.com/foomo/contentserver/pkg/repo"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
)
|
||||
|
||||
const sourceSocketServer = "socketserver"
|
||||
|
||||
type Socket struct {
|
||||
l *zap.Logger
|
||||
repo *repo.Repo
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// NewSocket returns a shiny new socket server
|
||||
func NewSocket(l *zap.Logger, repo *repo.Repo) *Socket {
|
||||
inst := &Socket{
|
||||
l: l.Named("socket"),
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *Socket) Serve(conn net.Conn) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if err, ok := r.(error); ok {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
h.l.Error("panic in handle connection", zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
h.l.Error("panic in handle connection", zap.String("error", fmt.Sprint(r)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// h.l.Debug("socketServer.handleConnection")
|
||||
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Inc()
|
||||
|
||||
var (
|
||||
headerBuffer [1]byte
|
||||
header = ""
|
||||
)
|
||||
for {
|
||||
// let us read with 1 byte steps on conn until we find "{"
|
||||
_, readErr := conn.Read(headerBuffer[0:])
|
||||
if errors.Is(readErr, io.EOF) {
|
||||
// client closed the connection
|
||||
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
|
||||
return
|
||||
} else if readErr != nil {
|
||||
h.l.Error("failed to read from connection", zap.Error(readErr))
|
||||
return
|
||||
}
|
||||
// read next byte
|
||||
current := headerBuffer[0:]
|
||||
if string(current) == "{" {
|
||||
// json has started
|
||||
handler, jsonLength, headerErr := h.extractHandlerAndJSONLentgh(header)
|
||||
// reset header
|
||||
header = ""
|
||||
if headerErr != nil {
|
||||
h.l.Error("invalid request could not read header", zap.Error(headerErr))
|
||||
encodedErr, encodingErr := h.encodeReply(responses.NewError(4, "invalid header "+headerErr.Error()))
|
||||
if encodingErr == nil {
|
||||
h.writeResponse(conn, encodedErr)
|
||||
} else {
|
||||
h.l.Error("could not respond to invalid request", zap.Error(encodingErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
h.l.Debug("found json", zap.Int("length", jsonLength))
|
||||
if jsonLength > 0 {
|
||||
var (
|
||||
// let us try to read some json
|
||||
jsonBytes = make([]byte, jsonLength)
|
||||
jsonLengthCurrent = 1
|
||||
readRound = 0
|
||||
)
|
||||
|
||||
// that is "{"
|
||||
jsonBytes[0] = 123
|
||||
|
||||
for jsonLengthCurrent < jsonLength {
|
||||
readRound++
|
||||
readLength, jsonReadErr := conn.Read(jsonBytes[jsonLengthCurrent:jsonLength])
|
||||
if jsonReadErr != nil {
|
||||
// @fixme we need to force a read timeout (SetReadDeadline?), if expected jsonLength is lower than really sent bytes (e.g. if client implements protocol wrong)
|
||||
// @todo should we check for io.EOF here
|
||||
h.l.Error("could not read json - giving up with this client connection", zap.Error(jsonReadErr))
|
||||
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
|
||||
return
|
||||
}
|
||||
jsonLengthCurrent += readLength
|
||||
h.l.Debug("read cycle status",
|
||||
zap.Int("jsonLengthCurrent", jsonLengthCurrent),
|
||||
zap.Int("jsonLength", jsonLength),
|
||||
zap.Int("readRound", readRound),
|
||||
)
|
||||
}
|
||||
|
||||
h.l.Debug("read json", zap.Int("length", len(jsonBytes)))
|
||||
|
||||
h.writeResponse(conn, h.execute(handler, jsonBytes))
|
||||
// note: connection remains open
|
||||
continue
|
||||
}
|
||||
h.l.Error("can not read empty json")
|
||||
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
|
||||
return
|
||||
}
|
||||
// adding to header byte by byte
|
||||
header += string(headerBuffer[0:])
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Private methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *Socket) extractHandlerAndJSONLentgh(header string) (route Route, jsonLength int, err error) {
|
||||
headerParts := strings.Split(header, ":")
|
||||
if len(headerParts) != 2 {
|
||||
return "", 0, errors.New("invalid header")
|
||||
}
|
||||
jsonLength, err = strconv.Atoi(headerParts[1])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not parse length in header: %q", header)
|
||||
}
|
||||
return Route(headerParts[0]), jsonLength, err
|
||||
}
|
||||
|
||||
func (h *Socket) execute(route Route, jsonBytes []byte) (reply []byte) {
|
||||
h.l.Debug("incoming json buffer", zap.Int("length", len(jsonBytes)))
|
||||
|
||||
if route == RouteGetRepo {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
)
|
||||
h.repo.WriteRepoBytes(&b)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
reply, handlingError := h.handleRequest(h.repo, route, jsonBytes, sourceSocketServer)
|
||||
if handlingError != nil {
|
||||
h.l.Error("socketServer.execute failed", zap.Error(handlingError))
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
func (h *Socket) writeResponse(conn net.Conn, reply []byte) {
|
||||
headerBytes := []byte(strconv.Itoa(len(reply)))
|
||||
reply = append(headerBytes, reply...)
|
||||
h.l.Debug("replying", zap.String("reply", string(reply)))
|
||||
n, writeError := conn.Write(reply)
|
||||
if writeError != nil {
|
||||
h.l.Error("socketServer.writeResponse: could not write reply", zap.Error(writeError))
|
||||
return
|
||||
}
|
||||
if n < len(reply) {
|
||||
h.l.Error("socketServer.writeResponse: write too short",
|
||||
zap.Int("got", n),
|
||||
zap.Int("expected", len(reply)),
|
||||
)
|
||||
return
|
||||
}
|
||||
h.l.Debug("replied. waiting for next request on open connection")
|
||||
}
|
||||
|
||||
func (h *Socket) handleRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) ([]byte, error) {
|
||||
start := time.Now()
|
||||
|
||||
reply, err := h.executeRequest(r, route, jsonBytes, source)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "error"
|
||||
}
|
||||
|
||||
metrics.ServiceRequestCounter.WithLabelValues(string(route), result, source).Inc()
|
||||
metrics.ServiceRequestDuration.WithLabelValues(string(route), result, source).Observe(time.Since(start).Seconds())
|
||||
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (h *Socket) executeRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) (replyBytes []byte, err error) {
|
||||
var (
|
||||
reply interface{}
|
||||
apiErr error
|
||||
jsonErr error
|
||||
processIfJSONIsOk = func(err error, processingFunc func()) {
|
||||
if err != nil {
|
||||
jsonErr = err
|
||||
return
|
||||
}
|
||||
processingFunc()
|
||||
}
|
||||
)
|
||||
metrics.ContentRequestCounter.WithLabelValues(source).Inc()
|
||||
|
||||
// handle and process
|
||||
switch route {
|
||||
// case RouteGetRepo: // This case is handled prior to handleRequest being called.
|
||||
// since the resulting bytes are written directly in to the http.ResponseWriter / net.Connection
|
||||
case RouteGetURIs:
|
||||
getURIRequest := &requests.URIs{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &getURIRequest), func() {
|
||||
reply = r.GetURIs(getURIRequest.Dimension, getURIRequest.IDs)
|
||||
})
|
||||
case RouteGetContent:
|
||||
contentRequest := &requests.Content{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &contentRequest), func() {
|
||||
reply, apiErr = r.GetContent(contentRequest)
|
||||
})
|
||||
case RouteGetNodes:
|
||||
nodesRequest := &requests.Nodes{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &nodesRequest), func() {
|
||||
reply = r.GetNodes(nodesRequest)
|
||||
})
|
||||
case RouteUpdate:
|
||||
updateRequest := &requests.Update{}
|
||||
processIfJSONIsOk(json.Unmarshal(jsonBytes, &updateRequest), func() {
|
||||
reply = r.Update()
|
||||
})
|
||||
|
||||
default:
|
||||
reply = responses.NewError(1, "unknown handler: "+string(route))
|
||||
}
|
||||
|
||||
// error handling
|
||||
if jsonErr != nil {
|
||||
h.l.Error("could not read incoming json", zap.Error(jsonErr))
|
||||
reply = responses.NewError(2, "could not read incoming json "+jsonErr.Error())
|
||||
} else if apiErr != nil {
|
||||
h.l.Error("an API error occurred", zap.Error(apiErr))
|
||||
reply = responses.NewError(3, "internal error "+apiErr.Error())
|
||||
}
|
||||
|
||||
return h.encodeReply(reply)
|
||||
}
|
||||
|
||||
// encodeReply takes an interface and encodes it as JSON
|
||||
// it returns the resulting JSON and a marshalling error
|
||||
func (h *Socket) encodeReply(reply interface{}) (replyBytes []byte, err error) {
|
||||
replyBytes, err = json.Marshal(map[string]interface{}{
|
||||
"reply": reply,
|
||||
})
|
||||
if err != nil {
|
||||
h.l.Error("could not encode reply", zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
100
pkg/metrics/metrics.go
Normal file
100
pkg/metrics/metrics.go
Normal file
@ -0,0 +1,100 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "contentserver"
|
||||
|
||||
metricLabelHandler = "handler"
|
||||
metricLabelStatus = "status"
|
||||
metricLabelSource = "source"
|
||||
metricLabelRemote = "remote"
|
||||
)
|
||||
|
||||
// Metrics is the structure that holds all prometheus metrics
|
||||
var (
|
||||
// InvalidNodeTreeRequests counts the number of invalid tree node requests
|
||||
InvalidNodeTreeRequests = newCounterVec(
|
||||
"invalid_node_tree_request_count",
|
||||
"Counts the number of invalid tree nodes for a specific node",
|
||||
)
|
||||
// ServiceRequestCounter count the number of requests for each service function
|
||||
ServiceRequestCounter = newCounterVec(
|
||||
"service_request_count",
|
||||
"Count of requests for each handler",
|
||||
metricLabelHandler, metricLabelStatus, metricLabelSource,
|
||||
)
|
||||
// ServiceRequestDuration observe the duration of requests for each service function
|
||||
ServiceRequestDuration = newSummaryVec(
|
||||
"service_request_duration_seconds",
|
||||
"Seconds to unmarshal requests, execute a service function and marshal its reponses",
|
||||
metricLabelHandler, metricLabelStatus, metricLabelSource,
|
||||
)
|
||||
// UpdatesCompletedCounter count the number of rejected updates
|
||||
UpdatesCompletedCounter = newCounterVec(
|
||||
"updates_completed_count",
|
||||
"Number of updates that were successfully completed",
|
||||
)
|
||||
// UpdatesFailedCounter count the number of updates that had an error
|
||||
UpdatesFailedCounter = newCounterVec(
|
||||
"updates_failed_count",
|
||||
"Number of updates that failed due to an error",
|
||||
)
|
||||
// UpdateDuration observe the duration of each repo.update() call
|
||||
UpdateDuration = newSummaryVec(
|
||||
"update_duration_seconds",
|
||||
"Duration in seconds for each successful repo.update() call",
|
||||
)
|
||||
// ContentRequestCounter count the total number of content requests
|
||||
ContentRequestCounter = newCounterVec(
|
||||
"content_request_count",
|
||||
"Number of requests for content",
|
||||
metricLabelSource,
|
||||
)
|
||||
// NumSocketsGauge keep track of the total number of open sockets
|
||||
NumSocketsGauge = newGaugeVec(
|
||||
"num_sockets_total",
|
||||
"Total number of currently open socket connections",
|
||||
metricLabelRemote,
|
||||
)
|
||||
// HistoryPersistFailedCounter count the number of failed attempts to persist the content history
|
||||
HistoryPersistFailedCounter = newCounterVec(
|
||||
"history_persist_failed_count",
|
||||
"Number of failures to store the content history on the filesystem",
|
||||
)
|
||||
)
|
||||
|
||||
func newSummaryVec(name, help string, labels ...string) *prometheus.SummaryVec {
|
||||
vec := prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Help: help,
|
||||
}, labels)
|
||||
prometheus.MustRegister(vec)
|
||||
return vec
|
||||
}
|
||||
|
||||
func newCounterVec(name, help string, labels ...string) *prometheus.CounterVec {
|
||||
vec := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Help: help,
|
||||
}, labels)
|
||||
prometheus.MustRegister(vec)
|
||||
return vec
|
||||
}
|
||||
|
||||
func newGaugeVec(name, help string, labels ...string) *prometheus.GaugeVec {
|
||||
vec := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Help: help,
|
||||
}, labels)
|
||||
prometheus.MustRegister(vec)
|
||||
return vec
|
||||
}
|
||||
12
pkg/repo/dimension.go
Normal file
12
pkg/repo/dimension.go
Normal file
@ -0,0 +1,12 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/foomo/contentserver/content"
|
||||
)
|
||||
|
||||
// Dimension dimension in a repo
|
||||
type Dimension struct {
|
||||
Directory map[string]*content.RepoNode
|
||||
URIDirectory map[string]*content.RepoNode
|
||||
Node *content.RepoNode
|
||||
}
|
||||
180
pkg/repo/history.go
Normal file
180
pkg/repo/history.go
Normal file
@ -0,0 +1,180 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
HistoryRepoJSONPrefix = "contentserver-repo-"
|
||||
HistoryRepoJSONSuffix = ".json"
|
||||
)
|
||||
|
||||
type (
|
||||
History struct {
|
||||
l *zap.Logger
|
||||
historyDir string
|
||||
historyLimit int
|
||||
currentMutext sync.RWMutex
|
||||
}
|
||||
HistoryOption func(*History)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Options
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func HistoryWithHistoryLimit(v int) HistoryOption {
|
||||
return func(o *History) {
|
||||
o.historyLimit = v
|
||||
}
|
||||
}
|
||||
|
||||
func HistoryWithHistoryDir(v string) HistoryOption {
|
||||
return func(o *History) {
|
||||
o.historyDir = v
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func NewHistory(l *zap.Logger, opts ...HistoryOption) *History {
|
||||
inst := &History{
|
||||
l: l,
|
||||
historyDir: "/var/lib/contentserver",
|
||||
historyLimit: 2,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(inst)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *History) Add(jsonBytes []byte) error {
|
||||
backupFilename := path.Join(h.historyDir, HistoryRepoJSONPrefix+time.Now().Format(time.RFC3339Nano)+HistoryRepoJSONSuffix)
|
||||
currentFilename := h.GetCurrentFilename()
|
||||
|
||||
if err := os.MkdirAll(path.Dir(backupFilename), 0700); err != nil {
|
||||
return errors.Wrap(err, "failed to create history dir")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupFilename, jsonBytes, 0600); err != nil {
|
||||
return errors.Wrap(err, "failed to write backup history file")
|
||||
}
|
||||
|
||||
h.l.Debug("writing files",
|
||||
zap.String("backup", backupFilename),
|
||||
zap.String("current", currentFilename),
|
||||
)
|
||||
|
||||
// current filename
|
||||
h.currentMutext.Lock()
|
||||
defer h.currentMutext.Unlock()
|
||||
if err := os.WriteFile(currentFilename, jsonBytes, 0600); err != nil {
|
||||
return errors.Wrap(err, "failed to write current history")
|
||||
}
|
||||
|
||||
if err := h.cleanup(); err != nil {
|
||||
return errors.Wrap(err, "failed to clean up history")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *History) GetCurrentFilename() string {
|
||||
return path.Join(h.historyDir, HistoryRepoJSONPrefix+"current"+HistoryRepoJSONSuffix)
|
||||
}
|
||||
|
||||
func (h *History) GetCurrent(buf *bytes.Buffer) error {
|
||||
h.currentMutext.RLock()
|
||||
defer h.currentMutext.RUnlock()
|
||||
f, err := os.Open(h.GetCurrentFilename())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(buf, f)
|
||||
return err
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Private methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (h *History) getHistory() (files []string, err error) {
|
||||
fileInfos, err := os.ReadDir(h.historyDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
currentName := h.GetCurrentFilename()
|
||||
for _, f := range fileInfos {
|
||||
if !f.IsDir() {
|
||||
filename := f.Name()
|
||||
if filename != currentName && (strings.HasPrefix(filename, HistoryRepoJSONPrefix) && strings.HasSuffix(filename, HistoryRepoJSONSuffix)) {
|
||||
files = append(files, path.Join(h.historyDir, filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(files)))
|
||||
return
|
||||
}
|
||||
|
||||
func (h *History) cleanup() error {
|
||||
files, err := h.getFilesForCleanup(h.historyLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
h.l.Debug("removing outdated backup", zap.String("file", f))
|
||||
err := os.Remove(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not remove file %s : %s", f, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *History) getFilesForCleanup(historyVersions int) (files []string, err error) {
|
||||
contentFiles, err := h.getHistory()
|
||||
if err != nil {
|
||||
return nil, errors.New("could not generate file cleanup list: " + err.Error())
|
||||
}
|
||||
|
||||
// fmt.Println("contentFiles:")
|
||||
// for _, f := range contentFiles {
|
||||
// fmt.Println(f)
|
||||
// }
|
||||
|
||||
// -1 to remove the current backup file from the number of items
|
||||
// so that only files with a timestamp are compared
|
||||
if len(contentFiles)-1 > historyVersions {
|
||||
for i := historyVersions + 1; i < len(contentFiles); i++ {
|
||||
// ignore current repository file to fall back on
|
||||
if contentFiles[i] == h.GetCurrentFilename() {
|
||||
continue
|
||||
}
|
||||
files = append(files, contentFiles[i])
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
72
pkg/repo/history_test.go
Normal file
72
pkg/repo/history_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestHistoryCurrent(t *testing.T) {
|
||||
var (
|
||||
h = testHistory(t)
|
||||
test = []byte("test")
|
||||
b bytes.Buffer
|
||||
)
|
||||
err := h.Add(test)
|
||||
require.NoError(t, err)
|
||||
err = h.GetCurrent(&b)
|
||||
require.NoError(t, err)
|
||||
if !bytes.Equal(b.Bytes(), test) {
|
||||
t.Fatalf("expected %q, got %q", string(test), b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryCleanup(t *testing.T) {
|
||||
h := testHistory(t)
|
||||
for i := 0; i < 50; i++ {
|
||||
err := h.Add([]byte(fmt.Sprint(i)))
|
||||
require.NoError(t, err)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
err := h.cleanup()
|
||||
require.NoError(t, err)
|
||||
files, err := h.getHistory()
|
||||
require.NoError(t, err)
|
||||
|
||||
// -1 for ignoring the current content backup file
|
||||
if len(files)-1 != 2 {
|
||||
t.Fatal("history too long", len(files), "instead of", 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryOrder(t *testing.T) {
|
||||
h := testHistory(t)
|
||||
h.historyDir = "testdata/order"
|
||||
|
||||
files, err := h.getHistory()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "testdata/order/contentserver-repo-current.json", files[0])
|
||||
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-23.json", files[1])
|
||||
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-22.json", files[2])
|
||||
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-21.json", files[3])
|
||||
}
|
||||
|
||||
func TestGetFilesForCleanup(t *testing.T) {
|
||||
h := testHistory(t)
|
||||
h.historyDir = "testdata/order"
|
||||
|
||||
files, err := h.getFilesForCleanup(2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-21.json", files[0])
|
||||
}
|
||||
|
||||
func testHistory(t *testing.T) *History {
|
||||
t.Helper()
|
||||
l := zaptest.NewLogger(t)
|
||||
return NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(t.TempDir()))
|
||||
}
|
||||
394
pkg/repo/loader.go
Normal file
394
pkg/repo/loader.go
Normal file
@ -0,0 +1,394 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/pkg/metrics"
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
ErrUpdateRejected = errors.New("update rejected: queue full")
|
||||
)
|
||||
|
||||
type updateResponse struct {
|
||||
repoRuntime int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Repo) PollRoutine(ctx context.Context) error {
|
||||
l := r.l.Named("routine.poll")
|
||||
ticker := time.NewTicker(r.pollInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.Debug("routine canceled")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
chanReponse := make(chan updateResponse)
|
||||
r.updateInProgressChannel <- chanReponse
|
||||
response := <-chanReponse
|
||||
if response.err == nil {
|
||||
l.Info("update success", zap.String("revision", r.pollVersion))
|
||||
} else {
|
||||
l.Error("update failed", zap.Error(response.err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) UpdateRoutine(ctx context.Context) error {
|
||||
l := r.l.Named("routine.update")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.Debug("routine canceled")
|
||||
return nil
|
||||
case resChan := <-r.updateInProgressChannel:
|
||||
start := time.Now()
|
||||
l := l.With(zap.String("run_id", uuid.New().String()))
|
||||
|
||||
l.Info("update started")
|
||||
|
||||
repoRuntime, err := r.update(context.WithoutCancel(ctx))
|
||||
if err != nil {
|
||||
l.Error("update failed", zap.Error(err))
|
||||
metrics.UpdatesFailedCounter.WithLabelValues().Inc()
|
||||
} else {
|
||||
if !r.Loaded() {
|
||||
r.loaded.Store(true)
|
||||
l.Info("initial update success")
|
||||
if r.onLoaded != nil {
|
||||
r.onLoaded()
|
||||
}
|
||||
} else {
|
||||
l.Info("update success")
|
||||
}
|
||||
metrics.UpdatesCompletedCounter.WithLabelValues().Inc()
|
||||
}
|
||||
|
||||
resChan <- updateResponse{
|
||||
repoRuntime: repoRuntime,
|
||||
err: err,
|
||||
}
|
||||
|
||||
metrics.UpdateDuration.WithLabelValues().Observe(time.Since(start).Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) DimensionUpdateRoutine(ctx context.Context) error {
|
||||
l := r.l.Named("routine.dimensionUpdate")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.Debug("routine canceled")
|
||||
return nil
|
||||
case newDimension := <-r.dimensionUpdateChannel:
|
||||
l.Debug("received a new dimension", zap.String("dimension", newDimension.Dimension))
|
||||
|
||||
err := r._updateDimension(newDimension.Dimension, newDimension.Node)
|
||||
l.Info("received result")
|
||||
if err != nil {
|
||||
l.Debug("update failed", zap.Error(err))
|
||||
}
|
||||
r.dimensionUpdateDoneChannel <- err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) updateDimension(dimension string, node *content.RepoNode) error {
|
||||
r.l.Debug("trying to push dimension into update channel", zap.String("dimension", dimension), zap.String("nodeName", node.Name))
|
||||
r.dimensionUpdateChannel <- &RepoDimension{
|
||||
Dimension: dimension,
|
||||
Node: node,
|
||||
}
|
||||
r.l.Debug("waiting for done signal")
|
||||
return <-r.dimensionUpdateDoneChannel
|
||||
}
|
||||
|
||||
// do not call directly, but only through channel
|
||||
func (r *Repo) _updateDimension(dimension string, newNode *content.RepoNode) error {
|
||||
newNode.WireParents()
|
||||
|
||||
var (
|
||||
newDirectory = make(map[string]*content.RepoNode)
|
||||
newURIDirectory = make(map[string]*content.RepoNode)
|
||||
err = buildDirectory(newNode, newDirectory, newURIDirectory)
|
||||
)
|
||||
if err != nil {
|
||||
return errors.New("update dimension \"" + dimension + "\" failed when building its directory:: " + err.Error())
|
||||
}
|
||||
err = wireAliases(newDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
// copy old datastructure to prevent concurrent map access
|
||||
// collect other dimension in the Directory
|
||||
newRepoDirectory := map[string]*Dimension{}
|
||||
for d, D := range r.Directory() {
|
||||
if d != dimension {
|
||||
newRepoDirectory[d] = D
|
||||
}
|
||||
}
|
||||
|
||||
// add the new dimension
|
||||
newRepoDirectory[dimension] = &Dimension{
|
||||
Node: newNode,
|
||||
Directory: newDirectory,
|
||||
URIDirectory: newURIDirectory,
|
||||
}
|
||||
r.SetDirectory(newRepoDirectory)
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
// @TODO: why not update only the dimension that has changed instead?
|
||||
// repo.Directory[dimension] = &Dimension{
|
||||
// Node: newNode,
|
||||
// Directory: newDirectory,
|
||||
// URIDirectory: newURIDirectory,
|
||||
// }
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDirectory(dirNode *content.RepoNode, directory map[string]*content.RepoNode, uRIDirectory map[string]*content.RepoNode) error {
|
||||
existingNode, ok := directory[dirNode.ID]
|
||||
if ok {
|
||||
return errors.New("duplicate node with id:" + existingNode.ID)
|
||||
}
|
||||
directory[dirNode.ID] = dirNode
|
||||
// todo handle duplicate uris
|
||||
if _, thereIsAnExistingURINode := uRIDirectory[dirNode.URI]; thereIsAnExistingURINode {
|
||||
return errors.New("duplicate uri: " + dirNode.URI + " (bad node id: " + dirNode.ID + ")")
|
||||
}
|
||||
uRIDirectory[dirNode.URI] = dirNode
|
||||
for _, childNode := range dirNode.Nodes {
|
||||
err := buildDirectory(childNode, directory, uRIDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wireAliases(directory map[string]*content.RepoNode) error {
|
||||
for _, repoNode := range directory {
|
||||
if len(repoNode.LinkID) > 0 {
|
||||
if destinationNode, ok := directory[repoNode.LinkID]; ok {
|
||||
repoNode.URI = destinationNode.URI
|
||||
} else {
|
||||
return errors.New("that link id points nowhere " + repoNode.LinkID + " from " + repoNode.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) loadNodesFromJSON() (nodes map[string]*content.RepoNode, err error) {
|
||||
nodes = make(map[string]*content.RepoNode)
|
||||
err = json.Unmarshal(r.JSONBufferBytes(), &nodes)
|
||||
if err != nil {
|
||||
r.l.Error("Failed to deserialize nodes", zap.Error(err))
|
||||
return nil, errors.New("failed to deserialize nodes")
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (r *Repo) tryToRestoreCurrent() error {
|
||||
buffer := &bytes.Buffer{}
|
||||
err := r.history.GetCurrent(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.SetJSONBuffer(buffer)
|
||||
return r.loadJSONBytes()
|
||||
}
|
||||
|
||||
func (r *Repo) get(ctx context.Context, url string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create get repo request")
|
||||
}
|
||||
response, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get repo")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("bad response code from repository %q want %q", response.Status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Log.Info(ansi.Red + "RESETTING BUFFER" + ansi.Reset)
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
// Log.Info(ansi.Green + "LOADING DATA INTO BUFFER" + ansi.Reset)
|
||||
_, err = io.Copy(buffer, response.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to copy IO stream")
|
||||
}
|
||||
r.SetJSONBuffer(buffer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) update(ctx context.Context) (repoRuntime int64, err error) {
|
||||
startTimeRepo := time.Now().UnixNano()
|
||||
|
||||
repoURL := r.url
|
||||
if r.poll {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.url, nil)
|
||||
if err != nil {
|
||||
return repoRuntime, err
|
||||
}
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return repoRuntime, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return repoRuntime, errors.New("could not poll latest repo download url - non 200 response")
|
||||
}
|
||||
responseBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return repoRuntime, errors.New("could not poll latest repo download url, could not read body")
|
||||
}
|
||||
repoURL = string(responseBytes)
|
||||
if repoURL == r.pollVersion {
|
||||
r.l.Info(
|
||||
"repo is up to date",
|
||||
zap.String("pollVersion", r.pollVersion),
|
||||
)
|
||||
// already up to date
|
||||
return repoRuntime, nil
|
||||
}
|
||||
r.l.Info(
|
||||
"new repo poll version",
|
||||
zap.String("pollVersion", r.pollVersion),
|
||||
)
|
||||
}
|
||||
|
||||
err = r.get(ctx, repoURL)
|
||||
repoRuntime = time.Now().UnixNano() - startTimeRepo
|
||||
if err != nil {
|
||||
// we have no json to load - the repo server did not reply
|
||||
r.l.Debug("failed to load json", zap.Error(err))
|
||||
return repoRuntime, err
|
||||
}
|
||||
r.l.Debug("loading json", zap.String("server", repoURL), zap.Int("length", len(r.JSONBufferBytes())))
|
||||
nodes, err := r.loadNodesFromJSON()
|
||||
if err != nil {
|
||||
// could not load nodes from json
|
||||
return repoRuntime, err
|
||||
}
|
||||
err = r.loadNodes(nodes)
|
||||
if err != nil {
|
||||
// repo failed to load nodes
|
||||
return repoRuntime, err
|
||||
}
|
||||
if r.poll {
|
||||
r.pollVersion = repoURL
|
||||
}
|
||||
|
||||
// Persist the JSON buffer after successful update
|
||||
if err := r.history.Add(r.JSONBufferBytes()); err != nil {
|
||||
r.l.Error("Failed to persist repo after update", zap.Error(err))
|
||||
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
|
||||
} else {
|
||||
r.l.Info("Successfully persisted repo after update")
|
||||
}
|
||||
|
||||
return repoRuntime, nil
|
||||
}
|
||||
|
||||
// limit ressources and allow only one update request at once
|
||||
func (r *Repo) tryUpdate() (repoRuntime int64, err error) {
|
||||
c := make(chan updateResponse)
|
||||
select {
|
||||
case r.updateInProgressChannel <- c:
|
||||
r.l.Debug("update request added to queue")
|
||||
ur := <-c
|
||||
return ur.repoRuntime, ur.err
|
||||
default:
|
||||
r.l.Info("update request accepted, will be processed after the previous update")
|
||||
return 0, ErrUpdateRejected
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) loadJSONBytes() error {
|
||||
nodes, err := r.loadNodesFromJSON()
|
||||
if err != nil {
|
||||
data := r.JSONBufferBytes()
|
||||
|
||||
if len(data) > 10 {
|
||||
r.l.Debug("could not parse json",
|
||||
zap.String("jsonStart", string(data[:10])),
|
||||
zap.String("jsonStart", string(data[len(data)-10:])),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.loadNodes(nodes)
|
||||
if err == nil {
|
||||
errHistory := r.history.Add(r.JSONBufferBytes())
|
||||
if errHistory != nil {
|
||||
r.l.Error("Could not add valid JSON to history", zap.Error(errHistory))
|
||||
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
|
||||
} else {
|
||||
r.l.Info("added valid JSON to history")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) loadNodes(newNodes map[string]*content.RepoNode) error {
|
||||
var err error
|
||||
newDimensions := make([]string, 0, len(newNodes))
|
||||
for dimension, newNode := range newNodes {
|
||||
newDimensions = append(newDimensions, dimension)
|
||||
r.l.Debug("loading nodes for dimension", zap.String("dimension", dimension))
|
||||
errLoad := r.updateDimension(dimension, newNode)
|
||||
if errLoad != nil {
|
||||
err = multierr.Append(err, errLoad)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update dimension")
|
||||
}
|
||||
dimensionIsValid := func(dimension string) bool {
|
||||
for _, newDimension := range newDimensions {
|
||||
if dimension == newDimension {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// we need to throw away orphaned dimensions
|
||||
directory := map[string]*Dimension{}
|
||||
for dimension, value := range r.Directory() {
|
||||
if !dimensionIsValid(dimension) {
|
||||
r.l.Info("removing orphaned dimension", zap.String("dimension", dimension))
|
||||
continue
|
||||
}
|
||||
directory[dimension] = value
|
||||
}
|
||||
r.SetDirectory(directory)
|
||||
return nil
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
@ -9,26 +8,27 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/log"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
)
|
||||
|
||||
// GetMockData mock data to run a repo
|
||||
func GetMockData(t testing.TB) (server *httptest.Server, varDir string) {
|
||||
log.SelectedLevel = log.LevelError
|
||||
func GetMockData(tb testing.TB) (*httptest.Server, string) {
|
||||
tb.Helper()
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
mockDir := path.Dir(filename)
|
||||
|
||||
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
mockFilename := path.Join(mockDir, req.URL.Path[1:])
|
||||
http.ServeFile(w, req, mockFilename)
|
||||
}))
|
||||
varDir, err := ioutil.TempDir("", "content-server-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return server, varDir
|
||||
|
||||
go func() {
|
||||
<-tb.Context().Done()
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
return server, tb.TempDir()
|
||||
}
|
||||
|
||||
// MakeNodesRequest a request to get some nodes
|
||||
@ -38,7 +38,7 @@ func MakeNodesRequest() *requests.Nodes {
|
||||
Dimensions: []string{"dimension_foo"},
|
||||
},
|
||||
Nodes: map[string]*requests.Node{
|
||||
"test": &requests.Node{
|
||||
"test": {
|
||||
ID: "id-root",
|
||||
Dimension: "dimension_foo",
|
||||
MimeTypes: []string{},
|
||||
@ -67,7 +67,7 @@ func MakeValidContentRequest() *requests.Content {
|
||||
Groups: []string{},
|
||||
},
|
||||
Nodes: map[string]*requests.Node{
|
||||
"id-root": &requests.Node{
|
||||
"id-root": {
|
||||
ID: "id-root",
|
||||
Dimension: dimensions[0],
|
||||
MimeTypes: []string{"application/x-node"},
|
||||
@ -76,5 +76,4 @@ func MakeValidContentRequest() *requests.Content {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
51
pkg/repo/mock/repo-ok-exposehidden.json
Normal file
51
pkg/repo/mock/repo-ok-exposehidden.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"dimension_foo": {
|
||||
"id": "id-root",
|
||||
"name": "root node",
|
||||
"mimeType": "application\/x-node",
|
||||
"groups": null,
|
||||
"URI": "\/",
|
||||
"hidden": false,
|
||||
"linkId": null,
|
||||
"destinationId": null,
|
||||
"data": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"index": [
|
||||
"id-a",
|
||||
"id-b"
|
||||
],
|
||||
"nodes": {
|
||||
"id-a": {
|
||||
"id": "id-a",
|
||||
"name": "node a",
|
||||
"mimeType": "application\/x-node",
|
||||
"groups": null,
|
||||
"URI": "\/a",
|
||||
"hidden": false,
|
||||
"linkId": null,
|
||||
"destinationId": null,
|
||||
"data": {
|
||||
"baz": 1
|
||||
},
|
||||
"index": [],
|
||||
"nodes": {}
|
||||
},
|
||||
"id-b": {
|
||||
"id": "id-b",
|
||||
"name": "node b",
|
||||
"mimeType": "application\/x-node",
|
||||
"groups": null,
|
||||
"URI": "\/b",
|
||||
"hidden": true,
|
||||
"linkId": null,
|
||||
"destinationId": null,
|
||||
"data": {
|
||||
"b": "b"
|
||||
},
|
||||
"index": [],
|
||||
"nodes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
524
pkg/repo/repo.go
Normal file
524
pkg/repo/repo.go
Normal file
@ -0,0 +1,524 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/pkg/metrics"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const maxGetURIForNodeRecursionLevel = 1000
|
||||
|
||||
// Repo content repository
|
||||
type (
|
||||
Repo struct {
|
||||
l *zap.Logger
|
||||
url string
|
||||
poll bool
|
||||
pollInterval time.Duration
|
||||
pollVersion string
|
||||
onLoaded func()
|
||||
loaded *atomic.Bool
|
||||
history *History
|
||||
httpClient *http.Client
|
||||
dimensionUpdateChannel chan *RepoDimension
|
||||
dimensionUpdateDoneChannel chan error
|
||||
updateInProgressChannel chan chan updateResponse
|
||||
directory map[string]*Dimension
|
||||
directoryLock sync.RWMutex
|
||||
jsonBuffer *bytes.Buffer
|
||||
jsonBufferLock sync.RWMutex
|
||||
}
|
||||
Option func(*Repo)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Constructor
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func New(l *zap.Logger, url string, history *History, opts ...Option) *Repo {
|
||||
inst := &Repo{
|
||||
l: l.Named("repo"),
|
||||
url: url,
|
||||
poll: false,
|
||||
loaded: &atomic.Bool{},
|
||||
pollInterval: time.Minute,
|
||||
history: history,
|
||||
httpClient: http.DefaultClient,
|
||||
directory: map[string]*Dimension{},
|
||||
dimensionUpdateChannel: make(chan *RepoDimension),
|
||||
dimensionUpdateDoneChannel: make(chan error),
|
||||
updateInProgressChannel: make(chan chan updateResponse),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(inst)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Options
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func WithHTTPClient(v *http.Client) Option {
|
||||
return func(o *Repo) {
|
||||
o.httpClient = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithPoll(v bool) Option {
|
||||
return func(o *Repo) {
|
||||
o.poll = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithPollInterval(v time.Duration) Option {
|
||||
return func(o *Repo) {
|
||||
o.pollInterval = v
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Getter
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (r *Repo) Loaded() bool {
|
||||
return r.loaded.Load()
|
||||
}
|
||||
|
||||
func (r *Repo) Directory() map[string]*Dimension {
|
||||
r.directoryLock.RLock()
|
||||
defer r.directoryLock.RUnlock()
|
||||
return r.directory
|
||||
}
|
||||
|
||||
func (r *Repo) SetDirectory(v map[string]*Dimension) {
|
||||
r.directoryLock.Lock()
|
||||
defer r.directoryLock.Unlock()
|
||||
r.directory = v
|
||||
}
|
||||
|
||||
func (r *Repo) JSONBufferBytes() []byte {
|
||||
r.jsonBufferLock.RLock()
|
||||
defer r.jsonBufferLock.RUnlock()
|
||||
return r.jsonBuffer.Bytes()
|
||||
}
|
||||
|
||||
func (r *Repo) SetJSONBuffer(v *bytes.Buffer) {
|
||||
r.jsonBufferLock.Lock()
|
||||
defer r.jsonBufferLock.Unlock()
|
||||
r.jsonBuffer = v
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Public methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (r *Repo) OnLoaded(fn func()) {
|
||||
r.onLoaded = fn
|
||||
}
|
||||
|
||||
// GetURIs get many uris at once
|
||||
func (r *Repo) GetURIs(dimension string, ids []string) map[string]string {
|
||||
uris := map[string]string{}
|
||||
for _, id := range ids {
|
||||
uris[id] = r.getURI(dimension, id)
|
||||
}
|
||||
return uris
|
||||
}
|
||||
|
||||
// GetNodes get nodes
|
||||
func (r *Repo) GetNodes(nodes *requests.Nodes) map[string]*content.Node {
|
||||
return r.getNodes(nodes.Nodes, nodes.Env)
|
||||
}
|
||||
|
||||
// GetContent resolves content and fetches nodes in one call. It combines those
|
||||
// two tasks for performance reasons.
|
||||
//
|
||||
// In the first step it uses r.URI to look up content in all given
|
||||
// r.Env.Dimensions of repo.Directory.
|
||||
//
|
||||
// In the second step it collects the requested nodes.
|
||||
//
|
||||
// those two steps are independent.
|
||||
func (r *Repo) GetContent(req *requests.Content) (*content.SiteContent, error) {
|
||||
// add more input validation
|
||||
err := r.validateContentRequest(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "repo.GetContent invalid request")
|
||||
}
|
||||
r.l.Debug("repo.GetContent", zap.String("URI", req.URI))
|
||||
c := content.NewSiteContent()
|
||||
resolved, resolvedURI, resolvedDimension, node := r.resolveContent(req.Env.Dimensions, req.URI)
|
||||
if resolved {
|
||||
if !node.CanBeAccessedByGroups(req.Env.Groups) {
|
||||
r.l.Warn("Resolved content cannot be accessed by specified group", zap.String("uri", req.URI))
|
||||
c.Status = content.StatusForbidden
|
||||
} else {
|
||||
r.l.Info("Content resolved", zap.String("uri", req.URI))
|
||||
c.Status = content.StatusOk
|
||||
c.Data = node.Data
|
||||
}
|
||||
c.MimeType = node.MimeType
|
||||
c.Dimension = resolvedDimension
|
||||
c.URI = resolvedURI
|
||||
c.Item = node.ToItem(req.DataFields)
|
||||
c.Path = node.GetPath(req.PathDataFields)
|
||||
// fetch URIs for all dimensions
|
||||
uris := make(map[string]string)
|
||||
for dimensionName := range r.Directory() {
|
||||
uris[dimensionName] = r.getURI(dimensionName, node.ID)
|
||||
}
|
||||
c.URIs = uris
|
||||
} else {
|
||||
r.l.Info("Content not found", zap.String("URI", req.URI))
|
||||
c.Status = content.StatusNotFound
|
||||
c.Dimension = req.Env.Dimensions[0]
|
||||
|
||||
r.l.Debug("Failed to resolve, falling back to default dimension",
|
||||
zap.String("uri", req.URI),
|
||||
zap.String("default_dimension", req.Env.Dimensions[0]),
|
||||
)
|
||||
// r.Env.Dimensions is validated => we can access it
|
||||
resolvedDimension = req.Env.Dimensions[0]
|
||||
}
|
||||
|
||||
// add navigation trees
|
||||
for _, node := range req.Nodes {
|
||||
if node.Dimension == "" {
|
||||
node.Dimension = resolvedDimension
|
||||
}
|
||||
}
|
||||
c.Nodes = r.getNodes(req.Nodes, req.Env)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetRepo get the whole repo in all dimensions
|
||||
func (r *Repo) GetRepo() map[string]*content.RepoNode {
|
||||
response := make(map[string]*content.RepoNode)
|
||||
for dimensionName, dimension := range r.Directory() {
|
||||
response[dimensionName] = dimension.Node
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// WriteRepoBytes get the whole repo in all dimensions
|
||||
// reads the JSON history file from the Filesystem and copies it directly in to the supplied buffer
|
||||
// the result is wrapped as service response, e.g: {"reply": <contentData>}
|
||||
func (r *Repo) WriteRepoBytes(w io.Writer) {
|
||||
filename := r.history.GetCurrentFilename()
|
||||
r.history.currentMutext.RLock()
|
||||
defer r.history.currentMutext.RUnlock()
|
||||
f, err := os.Open(r.history.GetCurrentFilename())
|
||||
if err != nil {
|
||||
r.l.Error("failed to open repo JSON", zap.Error(err), zap.String("history", filename))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write([]byte("{\"reply\":")); err != nil {
|
||||
r.l.Error("failed to write repo JSON prefix", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
r.l.Error("failed to serve repo JSON", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if _, err := w.Write([]byte("}")); err != nil {
|
||||
r.l.Error("failed to write repo JSON suffix", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) Update() (updateResponse *responses.Update) {
|
||||
floatSeconds := func(nanoSeconds int64) float64 {
|
||||
return float64(nanoSeconds) / float64(1000000000)
|
||||
}
|
||||
|
||||
r.l.Info("Update triggered")
|
||||
// Log.Info(ansi.Yellow + "BUFFER LENGTH BEFORE tryUpdate(): " + strconv.Itoa(len(repo.jsonBuf.Bytes())) + ansi.Reset)
|
||||
|
||||
start := time.Now()
|
||||
updateRepotime, err := r.tryUpdate()
|
||||
updateResponse = &responses.Update{}
|
||||
updateResponse.Stats.RepoRuntime = floatSeconds(updateRepotime)
|
||||
|
||||
if err != nil {
|
||||
updateResponse.Success = false
|
||||
updateResponse.Stats.NumberOfNodes = -1
|
||||
updateResponse.Stats.NumberOfURIs = -1
|
||||
|
||||
// let us try to restore the world from a file
|
||||
// Log.Info(ansi.Yellow + "BUFFER LENGTH AFTER ERROR: " + strconv.Itoa(len(r.jsonBuf.Bytes())) + ansi.Reset)
|
||||
// only try to restore if the update failed during processing
|
||||
|
||||
if !errors.Is(err, ErrUpdateRejected) {
|
||||
updateResponse.ErrorMessage = err.Error()
|
||||
r.l.Error("Failed to update repository", zap.Error(err))
|
||||
|
||||
restoreErr := r.tryToRestoreCurrent()
|
||||
if restoreErr != nil {
|
||||
r.l.Error("Failed to restore preceding repository version", zap.Error(restoreErr))
|
||||
} else {
|
||||
r.l.Info("Successfully restored current repository from local history")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateResponse.Success = true
|
||||
// persist the currently loaded one
|
||||
historyErr := r.history.Add(r.JSONBufferBytes())
|
||||
if historyErr != nil {
|
||||
r.l.Error("Could not persist current repo in history", zap.Error(historyErr))
|
||||
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
|
||||
} else {
|
||||
r.l.Info("Successfully persisted current repo to history")
|
||||
}
|
||||
// add some stats
|
||||
for _, dimension := range r.Directory() {
|
||||
updateResponse.Stats.NumberOfNodes += len(dimension.Directory)
|
||||
updateResponse.Stats.NumberOfURIs += len(dimension.URIDirectory)
|
||||
}
|
||||
}
|
||||
updateResponse.Stats.OwnRuntime = floatSeconds(time.Since(start).Nanoseconds()) - updateResponse.Stats.RepoRuntime
|
||||
return updateResponse
|
||||
}
|
||||
|
||||
func (r *Repo) Start(ctx context.Context) error {
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
l := r.l.Named("start")
|
||||
|
||||
up := make(chan bool, 1)
|
||||
g.Go(func() error {
|
||||
l.Debug("starting update routine")
|
||||
up <- true
|
||||
return r.UpdateRoutine(gCtx)
|
||||
})
|
||||
l.Debug("waiting for UpdateRoutine")
|
||||
<-up
|
||||
|
||||
g.Go(func() error {
|
||||
l.Debug("starting dimension update routine")
|
||||
up <- true
|
||||
return r.DimensionUpdateRoutine(gCtx)
|
||||
})
|
||||
l.Debug("waiting for DimensionUpdateRoutine")
|
||||
<-up
|
||||
|
||||
l.Debug("trying to restore previous repo")
|
||||
if err := r.tryToRestoreCurrent(); errors.Is(err, os.ErrNotExist) {
|
||||
l.Info("previous repo content file does not exist")
|
||||
} else if err != nil {
|
||||
l.Warn("could not restore previous repo content", zap.Error(err))
|
||||
} else {
|
||||
l.Info("restored previous repo")
|
||||
}
|
||||
|
||||
if r.poll {
|
||||
g.Go(func() error {
|
||||
l.Debug("starting poll routine")
|
||||
return r.PollRoutine(gCtx)
|
||||
})
|
||||
}
|
||||
|
||||
if !r.Loaded() {
|
||||
l.Debug("trying to update initial state")
|
||||
if resp := r.Update(); !resp.Success {
|
||||
l.Error("failed to update initial state",
|
||||
zap.String("error", resp.ErrorMessage),
|
||||
zap.Int("num_modes", resp.Stats.NumberOfNodes),
|
||||
zap.Int("num_uris", resp.Stats.NumberOfURIs),
|
||||
zap.Float64("own_runtime", resp.Stats.OwnRuntime),
|
||||
zap.Float64("repo_runtime", resp.Stats.RepoRuntime),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ~ Private methods
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (r *Repo) getNodes(nodeRequests map[string]*requests.Node, env *requests.Env) map[string]*content.Node {
|
||||
var (
|
||||
path []*content.Item
|
||||
nodes = map[string]*content.Node{}
|
||||
)
|
||||
for nodeName, nodeRequest := range nodeRequests {
|
||||
if nodeName == "" || nodeRequest.ID == "" {
|
||||
r.l.Warn("invalid node request", zap.Error(errors.New("nodeName or nodeRequest.ID empty")))
|
||||
continue
|
||||
}
|
||||
r.l.Debug("adding node", zap.String("name", nodeName), zap.String("requestID", nodeRequest.ID))
|
||||
|
||||
groups := env.Groups
|
||||
if len(nodeRequest.Groups) > 0 {
|
||||
groups = nodeRequest.Groups
|
||||
}
|
||||
|
||||
dimensionNode, ok := r.Directory()[nodeRequest.Dimension]
|
||||
nodes[nodeName] = nil
|
||||
|
||||
if !ok && nodeRequest.Dimension == "" {
|
||||
r.l.Debug("Could not get dimension root node", zap.String("dimension", nodeRequest.Dimension))
|
||||
for _, dimension := range env.Dimensions {
|
||||
dimensionNode, ok = r.Directory()[dimension]
|
||||
if ok {
|
||||
r.l.Debug("Found root node in env.Dimensions", zap.String("dimension", dimension))
|
||||
break
|
||||
}
|
||||
r.l.Debug("Could NOT find root node in env.Dimensions", zap.String("dimension", dimension))
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
r.l.Error("could not get dimension root node", zap.String("nodeRequest.Dimension", nodeRequest.Dimension))
|
||||
continue
|
||||
}
|
||||
|
||||
treeNode, ok := dimensionNode.Directory[nodeRequest.ID]
|
||||
if !ok {
|
||||
r.l.Error("Invalid tree node requested",
|
||||
zap.String("nodeName", nodeName),
|
||||
zap.String("nodeID", nodeRequest.ID),
|
||||
)
|
||||
metrics.InvalidNodeTreeRequests.WithLabelValues().Inc()
|
||||
continue
|
||||
}
|
||||
nodes[nodeName] = r.getNode(treeNode, nodeRequest.Expand, nodeRequest.MimeTypes, path, 0, groups, nodeRequest.DataFields, nodeRequest.ExposeHiddenNodes)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// resolveContent find content in a repository
|
||||
func (r *Repo) resolveContent(dimensions []string, uri string) (resolved bool, resolvedURI string, resolvedDimension string, repoNode *content.RepoNode) {
|
||||
parts := strings.Split(uri, content.PathSeparator)
|
||||
r.l.Debug("repo.ResolveContent", zap.String("URI", uri))
|
||||
for i := len(parts); i > 0; i-- {
|
||||
testURI := strings.Join(parts[0:i], content.PathSeparator)
|
||||
if testURI == "" {
|
||||
testURI = content.PathSeparator
|
||||
}
|
||||
for _, dimension := range dimensions {
|
||||
if d, ok := r.Directory()[dimension]; ok {
|
||||
r.l.Debug("Checking node",
|
||||
zap.String("dimension", dimension),
|
||||
zap.String("URI", testURI),
|
||||
)
|
||||
if repoNode, ok := d.URIDirectory[testURI]; ok {
|
||||
resolved = true
|
||||
r.l.Debug("Node found", zap.String("URI", testURI), zap.String("destination", repoNode.DestinationID))
|
||||
if len(repoNode.DestinationID) > 0 {
|
||||
if destionationNode, destinationNodeOk := d.Directory[repoNode.DestinationID]; destinationNodeOk {
|
||||
repoNode = destionationNode
|
||||
}
|
||||
}
|
||||
return resolved, testURI, dimension, repoNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Repo) getURIForNode(dimension string, repoNode *content.RepoNode, recursionLevel int64) (uri string) {
|
||||
if len(repoNode.LinkID) == 0 {
|
||||
uri = repoNode.URI
|
||||
return
|
||||
}
|
||||
linkedNode, ok := r.Directory()[dimension].Directory[repoNode.LinkID]
|
||||
if ok {
|
||||
if recursionLevel > maxGetURIForNodeRecursionLevel {
|
||||
r.l.Error("maxGetURIForNodeRecursionLevel reached", zap.String("repoNode.ID", repoNode.ID), zap.String("linkID", repoNode.LinkID), zap.String("dimension", dimension))
|
||||
return ""
|
||||
}
|
||||
return r.getURIForNode(dimension, linkedNode, recursionLevel+1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Repo) getURI(dimension string, id string) string {
|
||||
directory, ok := r.Directory()[dimension]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
repoNode, ok := directory.Directory[id]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return r.getURIForNode(dimension, repoNode, 0)
|
||||
}
|
||||
|
||||
func (r *Repo) getNode(
|
||||
repoNode *content.RepoNode,
|
||||
expanded bool,
|
||||
mimeTypes []string,
|
||||
path []*content.Item,
|
||||
level int,
|
||||
groups []string,
|
||||
dataFields []string,
|
||||
exposeHiddenNodes bool,
|
||||
) *content.Node {
|
||||
node := content.NewNode()
|
||||
node.Item = repoNode.ToItem(dataFields)
|
||||
r.l.Debug("getNode", zap.String("ID", repoNode.ID))
|
||||
for _, childID := range repoNode.Index {
|
||||
childNode := repoNode.Nodes[childID]
|
||||
if (level == 0 || expanded || !expanded && childNode.InPath(path)) && (!childNode.Hidden || exposeHiddenNodes) && childNode.CanBeAccessedByGroups(groups) && childNode.IsOneOfTheseMimeTypes(mimeTypes) {
|
||||
node.Nodes[childID] = r.getNode(childNode, expanded, mimeTypes, path, level+1, groups, dataFields, exposeHiddenNodes)
|
||||
node.Index = append(node.Index, childID)
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (r *Repo) validateContentRequest(req *requests.Content) (err error) {
|
||||
if req == nil {
|
||||
return errors.New("request must not be nil")
|
||||
}
|
||||
if len(req.URI) == 0 {
|
||||
return errors.New("request URI must not be empty")
|
||||
}
|
||||
if req.Env == nil {
|
||||
return errors.New("request.Env must not be nil")
|
||||
}
|
||||
if len(req.Env.Dimensions) == 0 {
|
||||
return errors.New("request.Env.Dimensions must not be empty")
|
||||
}
|
||||
for _, envDimension := range req.Env.Dimensions {
|
||||
if !r.hasDimension(envDimension) {
|
||||
availableDimensions := make([]string, 0, len(r.Directory()))
|
||||
for availableDimension := range r.Directory() {
|
||||
availableDimensions = append(availableDimensions, availableDimension)
|
||||
}
|
||||
return errors.New(fmt.Sprint(
|
||||
"unknown dimension ", envDimension,
|
||||
" in r.Env must be one of ", availableDimensions,
|
||||
" repo has ", len(availableDimensions), " dimensions",
|
||||
))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) hasDimension(d string) bool {
|
||||
_, hasDimension := r.Directory()[d]
|
||||
return hasDimension
|
||||
}
|
||||
249
pkg/repo/repo_test.go
Normal file
249
pkg/repo/repo_test.go
Normal file
@ -0,0 +1,249 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/pkg/repo/mock"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func NewTestRepo(ctx context.Context, l *zap.Logger, url, varDir string) *Repo {
|
||||
h := NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(varDir))
|
||||
r := New(l, url, h)
|
||||
go r.Start(ctx) //nolint:errcheck
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return r
|
||||
}
|
||||
|
||||
func assertRepoIsEmpty(t *testing.T, r *Repo, empty bool) {
|
||||
t.Helper()
|
||||
if empty {
|
||||
if len(r.Directory()) > 0 {
|
||||
t.Fatal("directory should have been empty, but is not")
|
||||
}
|
||||
} else {
|
||||
if len(r.Directory()) == 0 {
|
||||
t.Fatal("directory is empty, but should have been not")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad404(t *testing.T) {
|
||||
var (
|
||||
l = zaptest.NewLogger(t)
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
url = mockServer.URL + "/repo-no-have"
|
||||
r = NewTestRepo(t.Context(), l, url, varDir)
|
||||
)
|
||||
|
||||
response := r.Update()
|
||||
if response.Success {
|
||||
t.Fatal("can not get a repo, if the server responds with a 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBrokenRepo(t *testing.T) {
|
||||
var (
|
||||
l = zaptest.NewLogger(t)
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
server = mockServer.URL + "/repo-broken-json.json"
|
||||
r = NewTestRepo(t.Context(), l, server, varDir)
|
||||
)
|
||||
|
||||
response := r.Update()
|
||||
if response.Success {
|
||||
t.Fatal("how could we load a broken json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRepo(t *testing.T) {
|
||||
var (
|
||||
l = zaptest.NewLogger(t)
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
server = mockServer.URL + "/repo-ok.json"
|
||||
r = NewTestRepo(t.Context(), l, server, varDir)
|
||||
)
|
||||
assertRepoIsEmpty(t, r, false)
|
||||
|
||||
response := r.Update()
|
||||
assertRepoIsEmpty(t, r, false)
|
||||
|
||||
if !response.Success {
|
||||
t.Fatal("could not load valid repo")
|
||||
}
|
||||
if response.Stats.OwnRuntime > response.Stats.RepoRuntime {
|
||||
t.Fatal("how could all take less time, than me alone")
|
||||
}
|
||||
if response.Stats.RepoRuntime < 0.05 {
|
||||
t.Fatal("the server was too fast")
|
||||
}
|
||||
|
||||
// see what happens if we try to start it up again
|
||||
// nr := NewTestRepo(l, server, varDir)
|
||||
// assertRepoIsEmpty(t, nr, false)
|
||||
}
|
||||
|
||||
func BenchmarkLoadRepo(b *testing.B) {
|
||||
var (
|
||||
l = zaptest.NewLogger(b)
|
||||
t = &testing.T{}
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
server = mockServer.URL + "/repo-ok.json"
|
||||
r = NewTestRepo(b.Context(), l, server, varDir)
|
||||
)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
response := r.Update()
|
||||
if len(r.Directory()) == 0 {
|
||||
b.Fatal("directory is empty, but should have been not")
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
b.Fatal("could not load valid repo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRepoDuplicateUris(t *testing.T) {
|
||||
var (
|
||||
l = zaptest.NewLogger(t)
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
server = mockServer.URL + "/repo-duplicate-uris.json"
|
||||
r = NewTestRepo(t.Context(), l, server, varDir)
|
||||
)
|
||||
|
||||
response := r.Update()
|
||||
require.False(t, response.Success, "there are duplicates, this repo update should have failed")
|
||||
|
||||
assert.Contains(t, response.ErrorMessage, "update dimension")
|
||||
}
|
||||
|
||||
func TestDimensionHygiene(t *testing.T) {
|
||||
l := zaptest.NewLogger(t)
|
||||
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-two-dimensions.json"
|
||||
r := NewTestRepo(t.Context(), l, server, varDir)
|
||||
|
||||
response := r.Update()
|
||||
require.True(t, response.Success, "well those two dimension should be fine")
|
||||
|
||||
r.url = mockServer.URL + "/repo-ok.json"
|
||||
response = r.Update()
|
||||
require.True(t, response.Success, "it is called repo ok")
|
||||
|
||||
assert.Lenf(t, r.Directory(), 1, "directory hygiene failed")
|
||||
}
|
||||
|
||||
func getTestRepo(t *testing.T, path string) *Repo {
|
||||
t.Helper()
|
||||
l := zaptest.NewLogger(t)
|
||||
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + path
|
||||
r := NewTestRepo(t.Context(), l, server, varDir)
|
||||
response := r.Update()
|
||||
|
||||
require.True(t, response.Success, "well those two dimension should be fine")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestGetNodes(t *testing.T) {
|
||||
r := getTestRepo(t, "/repo-two-dimensions.json")
|
||||
nodesRequest := mock.MakeNodesRequest()
|
||||
nodes := r.GetNodes(nodesRequest)
|
||||
testNode, ok := nodes["test"]
|
||||
|
||||
require.True(t, ok, "should be a node")
|
||||
|
||||
testData, ok := testNode.Item.Data["foo"]
|
||||
require.True(t, ok, "failed to fetch test data")
|
||||
|
||||
t.Log("testData", testData)
|
||||
}
|
||||
|
||||
func TestGetNodesExposeHidden(t *testing.T) {
|
||||
r := getTestRepo(t, "/repo-ok-exposehidden.json")
|
||||
nodesRequest := mock.MakeNodesRequest()
|
||||
nodesRequest.Nodes["test"].ExposeHiddenNodes = true
|
||||
nodes := r.GetNodes(nodesRequest)
|
||||
|
||||
testNode, ok := nodes["test"]
|
||||
require.True(t, ok, "should be a node")
|
||||
|
||||
_, ok = testNode.Item.Data["foo"]
|
||||
require.True(t, ok, "failed to fetch test data")
|
||||
|
||||
require.Len(t, testNode.Nodes, 2)
|
||||
}
|
||||
|
||||
func TestResolveContent(t *testing.T) {
|
||||
r := getTestRepo(t, "/repo-two-dimensions.json")
|
||||
contentRequest := mock.MakeValidContentRequest()
|
||||
siteContent, err := r.GetContent(contentRequest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, contentRequest.URI, siteContent.URI, "failed to resolve uri")
|
||||
}
|
||||
|
||||
func TestLinkIds(t *testing.T) {
|
||||
l := zaptest.NewLogger(t)
|
||||
|
||||
var (
|
||||
mockServer, varDir = mock.GetMockData(t)
|
||||
server = mockServer.URL + "/repo-link-ok.json"
|
||||
r = NewTestRepo(t.Context(), l, server, varDir)
|
||||
response = r.Update()
|
||||
)
|
||||
|
||||
if !response.Success {
|
||||
t.Fatal("those links should have been fine")
|
||||
}
|
||||
|
||||
r.url = mockServer.URL + "/repo-link-broken.json"
|
||||
response = r.Update()
|
||||
|
||||
if response.Success {
|
||||
t.Fatal("I do not think so")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidRequest(t *testing.T) {
|
||||
r := getTestRepo(t, "/repo-two-dimensions.json")
|
||||
|
||||
if r.validateContentRequest(mock.MakeValidContentRequest()) != nil {
|
||||
t.Fatal("failed validation a valid request")
|
||||
}
|
||||
|
||||
tests := map[string]*requests.Content{}
|
||||
|
||||
rEmptyURI := mock.MakeValidContentRequest()
|
||||
rEmptyURI.URI = ""
|
||||
tests["empty uri"] = rEmptyURI
|
||||
|
||||
rEmptyEnv := mock.MakeValidContentRequest()
|
||||
rEmptyEnv.Env = nil
|
||||
tests["empty env"] = rEmptyEnv
|
||||
|
||||
rEmptyEnvDimensions := mock.MakeValidContentRequest()
|
||||
rEmptyEnvDimensions.Env.Dimensions = []string{}
|
||||
tests["empty env dimensions"] = rEmptyEnvDimensions
|
||||
|
||||
// rNodesValidID := mock.MakeValidContentRequest()
|
||||
// rNodesValidID.Nodes["id-root"].Id = ""
|
||||
// tests["nodes must have a valid id"] = rNodesValidID
|
||||
|
||||
for comment, req := range tests {
|
||||
if r.validateContentRequest(req) == nil {
|
||||
t.Fatal(comment, "should have failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
10
pkg/repo/repodimension.go
Normal file
10
pkg/repo/repodimension.go
Normal file
@ -0,0 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/foomo/contentserver/content"
|
||||
)
|
||||
|
||||
type RepoDimension struct {
|
||||
Dimension string
|
||||
Node *content.RepoNode
|
||||
}
|
||||
15
pkg/utils/url.go
Normal file
15
pkg/utils/url.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func IsValidURL(str string) bool {
|
||||
u, err := url.Parse(str)
|
||||
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
|
||||
return err == nil && u.Scheme != "" && u.Host != ""
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const historyRepoJSONPrefix = "contentserver-repo-"
|
||||
const historyRepoJSONSuffix = ".json"
|
||||
const maxHistoryVersions = 20
|
||||
|
||||
type history struct {
|
||||
varDir string
|
||||
}
|
||||
|
||||
func newHistory(varDir string) *history {
|
||||
return &history{
|
||||
varDir: varDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *history) add(jsonBytes []byte) error {
|
||||
// historic file name
|
||||
filename := path.Join(h.varDir, historyRepoJSONPrefix+time.Now().Format(time.RFC3339Nano)+historyRepoJSONSuffix)
|
||||
err := ioutil.WriteFile(filename, jsonBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// current filename
|
||||
return ioutil.WriteFile(h.getCurrentFilename(), jsonBytes, 0644)
|
||||
}
|
||||
|
||||
func (h *history) getHistory() (files []string, err error) {
|
||||
files = []string{}
|
||||
fileInfos, err := ioutil.ReadDir(h.varDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
currentName := h.getCurrentFilename()
|
||||
for _, f := range fileInfos {
|
||||
if !f.IsDir() {
|
||||
filename := f.Name()
|
||||
if filename != currentName && (strings.HasPrefix(filename, historyRepoJSONPrefix) && strings.HasSuffix(filename, historyRepoJSONSuffix)) {
|
||||
files = append(files, path.Join(h.varDir, filename))
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(files)))
|
||||
return
|
||||
}
|
||||
|
||||
func (h *history) cleanup() error {
|
||||
files, err := h.getFilesForCleanup(maxHistoryVersions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
err := os.Remove(f)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("could not remove file %s : %s", f, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *history) getFilesForCleanup(historyVersions int) (files []string, err error) {
|
||||
contentFiles, err := h.getHistory()
|
||||
if err != nil {
|
||||
return nil, errors.New("could not generate file cleanup list: " + err.Error())
|
||||
}
|
||||
if len(contentFiles) > historyVersions {
|
||||
for i := historyVersions; i < len(contentFiles); i++ {
|
||||
// ignore current repository file to fall back on
|
||||
if contentFiles[i] == h.getCurrentFilename() {
|
||||
continue
|
||||
}
|
||||
files = append(files, contentFiles[i])
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (h *history) getCurrentFilename() string {
|
||||
return path.Join(h.varDir, historyRepoJSONPrefix+"current"+historyRepoJSONSuffix)
|
||||
}
|
||||
|
||||
func (h *history) getCurrent() (jsonBytes []byte, err error) {
|
||||
return ioutil.ReadFile(h.getCurrentFilename())
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testHistory() *history {
|
||||
tempDir, err := ioutil.TempDir(os.TempDir(), "contentserver-history-test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newHistory(tempDir)
|
||||
}
|
||||
|
||||
func TestHistoryCurrent(t *testing.T) {
|
||||
h := testHistory()
|
||||
test := []byte("test")
|
||||
h.add(test)
|
||||
current, err := h.getCurrent()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(current, test) != 0 {
|
||||
t.Fatal(fmt.Sprintf("expected %q, got %q", string(test), string(current)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryCleanup(t *testing.T) {
|
||||
h := testHistory()
|
||||
for i := 0; i < 50; i++ {
|
||||
h.add([]byte(fmt.Sprint(i)))
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
h.cleanup()
|
||||
files, err := h.getHistory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != maxHistoryVersions {
|
||||
t.Fatal("history too long", len(files), "instead of", maxHistoryVersions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryOrder(t *testing.T) {
|
||||
h := testHistory()
|
||||
h.varDir = "testdata/order"
|
||||
|
||||
files, err := h.getHistory()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("error not expected")
|
||||
}
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-current.json", files[0])
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-23.json", files[1])
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-22.json", files[2])
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-21.json", files[3])
|
||||
}
|
||||
|
||||
func TestGetFilesForCleanup(t *testing.T) {
|
||||
h := testHistory()
|
||||
h.varDir = "testdata/order"
|
||||
|
||||
files, err := h.getFilesForCleanup(2)
|
||||
if err != nil {
|
||||
t.Fatal("error not expected")
|
||||
}
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-22.json", files[0])
|
||||
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-21.json", files[1])
|
||||
}
|
||||
|
||||
|
||||
func assertStringEqual(t *testing.T, expected, actual string) {
|
||||
if expected != actual {
|
||||
t.Errorf("expected string %s differs from the actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
197
repo/loader.go
197
repo/loader.go
@ -1,197 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/log"
|
||||
)
|
||||
|
||||
func (repo *Repo) updateRoutine() {
|
||||
go func() {
|
||||
for {
|
||||
log.Debug("update routine is about to select")
|
||||
select {
|
||||
case newDimension := <-repo.updateChannel:
|
||||
log.Debug("update routine received a new dimension: " + newDimension.Dimension)
|
||||
err := repo._updateDimension(newDimension.Dimension, newDimension.Node)
|
||||
log.Debug("update routine received result")
|
||||
if err != nil {
|
||||
log.Debug(" update routine error: " + err.Error())
|
||||
}
|
||||
repo.updateDoneChannel <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (repo *Repo) updateDimension(dimension string, node *content.RepoNode) error {
|
||||
repo.updateChannel <- &repoDimension{
|
||||
Dimension: dimension,
|
||||
Node: node,
|
||||
}
|
||||
return <-repo.updateDoneChannel
|
||||
}
|
||||
|
||||
// do not call directly, but only through channel
|
||||
func (repo *Repo) _updateDimension(dimension string, newNode *content.RepoNode) error {
|
||||
newNode.WireParents()
|
||||
newDirectory := make(map[string]*content.RepoNode)
|
||||
newURIDirectory := make(map[string]*content.RepoNode)
|
||||
|
||||
err := builDirectory(newNode, newDirectory, newURIDirectory)
|
||||
if err != nil {
|
||||
return errors.New("update dimension \"" + dimension + "\" failed when building its directory:: " + err.Error())
|
||||
}
|
||||
err = wireAliases(newDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.Directory[dimension] = &Dimension{
|
||||
Node: newNode,
|
||||
Directory: newDirectory,
|
||||
URIDirectory: newURIDirectory,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func builDirectory(dirNode *content.RepoNode, directory map[string]*content.RepoNode, uRIDirectory map[string]*content.RepoNode) error {
|
||||
log.Debug("repo.buildDirectory: " + dirNode.ID)
|
||||
existingNode, ok := directory[dirNode.ID]
|
||||
if ok {
|
||||
return errors.New("duplicate node with id:" + existingNode.ID)
|
||||
}
|
||||
directory[dirNode.ID] = dirNode
|
||||
//todo handle duplicate uris
|
||||
if _, thereIsAnExistingURINode := uRIDirectory[dirNode.URI]; thereIsAnExistingURINode {
|
||||
return errors.New("duplicate uri: " + dirNode.URI + " (bad node id: " + dirNode.ID + ")")
|
||||
}
|
||||
uRIDirectory[dirNode.URI] = dirNode
|
||||
for _, childNode := range dirNode.Nodes {
|
||||
err := builDirectory(childNode, directory, uRIDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wireAliases(directory map[string]*content.RepoNode) error {
|
||||
for _, repoNode := range directory {
|
||||
if len(repoNode.LinkID) > 0 {
|
||||
if destinationNode, ok := directory[repoNode.LinkID]; ok {
|
||||
repoNode.URI = destinationNode.URI
|
||||
} else {
|
||||
return errors.New("that link id points nowhere " + repoNode.LinkID + " from " + repoNode.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadNodesFromJSON(jsonBytes []byte) (nodes map[string]*content.RepoNode, err error) {
|
||||
nodes = make(map[string]*content.RepoNode)
|
||||
err = json.Unmarshal(jsonBytes, &nodes)
|
||||
return nodes, err
|
||||
}
|
||||
|
||||
func (repo *Repo) tryToRestoreCurrent() error {
|
||||
currentJSONBytes, err := repo.history.getCurrent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.loadJSONBytes(currentJSONBytes)
|
||||
}
|
||||
|
||||
func get(URL string) (data []byte, err error) {
|
||||
response, err := http.Get(URL)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return data, fmt.Errorf("Bad HTTP Response: %q", response.Status)
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
func (repo *Repo) update() (repoRuntime int64, jsonBytes []byte, err error) {
|
||||
startTimeRepo := time.Now().UnixNano()
|
||||
jsonBytes, err = get(repo.server)
|
||||
repoRuntime = time.Now().UnixNano() - startTimeRepo
|
||||
if err != nil {
|
||||
// we have no json to load - the repo server did not reply
|
||||
log.Debug("we have no json to load - the repo server did not reply", err)
|
||||
return repoRuntime, jsonBytes, err
|
||||
}
|
||||
log.Debug("loading json from: "+repo.server, string(jsonBytes))
|
||||
nodes, err := loadNodesFromJSON(jsonBytes)
|
||||
if err != nil {
|
||||
// could not load nodes from json
|
||||
return repoRuntime, jsonBytes, err
|
||||
}
|
||||
err = repo.loadNodes(nodes)
|
||||
if err != nil {
|
||||
// repo failed to load nodes
|
||||
return repoRuntime, jsonBytes, err
|
||||
}
|
||||
return repoRuntime, jsonBytes, nil
|
||||
}
|
||||
|
||||
func (repo *Repo) loadJSONBytes(jsonBytes []byte) error {
|
||||
nodes, err := loadNodesFromJSON(jsonBytes)
|
||||
if err != nil {
|
||||
log.Debug("could not parse json", string(jsonBytes))
|
||||
return err
|
||||
}
|
||||
err = repo.loadNodes(nodes)
|
||||
if err == nil {
|
||||
historyErr := repo.history.add(jsonBytes)
|
||||
if historyErr != nil {
|
||||
log.Warning("could not add valid json to history:" + historyErr.Error())
|
||||
} else {
|
||||
log.Record("added valid json to history")
|
||||
}
|
||||
cleanUpErr := repo.history.cleanup()
|
||||
if cleanUpErr != nil {
|
||||
log.Warning("an error occured while cleaning up my history:", cleanUpErr)
|
||||
} else {
|
||||
log.Record("cleaned up history")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *Repo) loadNodes(newNodes map[string]*content.RepoNode) error {
|
||||
newDimensions := []string{}
|
||||
for dimension, newNode := range newNodes {
|
||||
newDimensions = append(newDimensions, dimension)
|
||||
log.Debug("loading nodes for dimension " + dimension)
|
||||
loadErr := repo.updateDimension(dimension, newNode)
|
||||
if loadErr != nil {
|
||||
log.Debug(" failed to load " + dimension + ": " + loadErr.Error())
|
||||
return loadErr
|
||||
}
|
||||
}
|
||||
dimensionIsValid := func(dimension string) bool {
|
||||
for _, newDimension := range newDimensions {
|
||||
if dimension == newDimension {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// we need to throw away orphaned dimensions
|
||||
for dimension := range repo.Directory {
|
||||
if !dimensionIsValid(dimension) {
|
||||
log.Notice("removing orphaned dimension:" + dimension)
|
||||
delete(repo.Directory, dimension)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
328
repo/repo.go
328
repo/repo.go
@ -1,328 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/contentserver/content"
|
||||
"github.com/foomo/contentserver/log"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
"github.com/foomo/contentserver/responses"
|
||||
)
|
||||
|
||||
// Dimension dimension in a repo
|
||||
type Dimension struct {
|
||||
Directory map[string]*content.RepoNode
|
||||
URIDirectory map[string]*content.RepoNode
|
||||
Node *content.RepoNode
|
||||
}
|
||||
|
||||
// Repo content repositiory
|
||||
type Repo struct {
|
||||
server string
|
||||
Directory map[string]*Dimension
|
||||
updateChannel chan *repoDimension
|
||||
updateDoneChannel chan error
|
||||
history *history
|
||||
}
|
||||
|
||||
type repoDimension struct {
|
||||
Dimension string
|
||||
Node *content.RepoNode
|
||||
}
|
||||
|
||||
// NewRepo constructor
|
||||
func NewRepo(server string, varDir string) *Repo {
|
||||
log.Notice("creating new repo for " + server)
|
||||
log.Notice(" using var dir:" + varDir)
|
||||
repo := &Repo{
|
||||
server: server,
|
||||
Directory: map[string]*Dimension{},
|
||||
history: newHistory(varDir),
|
||||
updateChannel: make(chan *repoDimension),
|
||||
updateDoneChannel: make(chan error),
|
||||
}
|
||||
go repo.updateRoutine()
|
||||
log.Record("trying to restore pervious state")
|
||||
restoreErr := repo.tryToRestoreCurrent()
|
||||
if restoreErr != nil {
|
||||
log.Record(" could not restore previous repo content:" + restoreErr.Error())
|
||||
} else {
|
||||
log.Record(" restored previous repo content")
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
// GetURIs get many uris at once
|
||||
func (repo *Repo) GetURIs(dimension string, ids []string) map[string]string {
|
||||
uris := map[string]string{}
|
||||
for _, id := range ids {
|
||||
uris[id] = repo.getURI(dimension, id)
|
||||
}
|
||||
return uris
|
||||
}
|
||||
|
||||
// GetNodes get nodes
|
||||
func (repo *Repo) GetNodes(r *requests.Nodes) map[string]*content.Node {
|
||||
return repo.getNodes(r.Nodes, r.Env)
|
||||
}
|
||||
|
||||
func (repo *Repo) getNodes(nodeRequests map[string]*requests.Node, env *requests.Env) map[string]*content.Node {
|
||||
nodes := map[string]*content.Node{}
|
||||
path := []*content.Item{}
|
||||
for nodeName, nodeRequest := range nodeRequests {
|
||||
log.Debug(" adding node " + nodeName + " " + nodeRequest.ID)
|
||||
|
||||
groups := env.Groups
|
||||
if len(nodeRequest.Groups) > 0 {
|
||||
groups = nodeRequest.Groups
|
||||
}
|
||||
|
||||
dimensionNode, ok := repo.Directory[nodeRequest.Dimension]
|
||||
nodes[nodeName] = nil
|
||||
|
||||
if !ok && nodeRequest.Dimension == "" {
|
||||
log.Debug(" could not get dimension root node for dimension " + nodeRequest.Dimension)
|
||||
for _, dimension := range env.Dimensions {
|
||||
dimensionNode, ok = repo.Directory[dimension]
|
||||
if ok {
|
||||
log.Debug(" searched for root node in env.dimension " + dimension + " with success")
|
||||
break
|
||||
}
|
||||
log.Debug(" searched for root node in env.dimension " + dimension + " without success")
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Warning("could not get dimension root node for nodeRequest.Dimension: " + nodeRequest.Dimension)
|
||||
continue
|
||||
}
|
||||
treeNode, ok := dimensionNode.Directory[nodeRequest.ID]
|
||||
if ok {
|
||||
nodes[nodeName] = repo.getNode(treeNode, nodeRequest.Expand, nodeRequest.MimeTypes, path, 0, groups, nodeRequest.DataFields)
|
||||
} else {
|
||||
log.Warning("you are requesting an invalid tree node for " + nodeName + " : " + nodeRequest.ID)
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetContent resolves content and fetches nodes in one call. It combines those
|
||||
// two tasks for performance reasons.
|
||||
//
|
||||
// In the first step it uses r.URI to look up content in all given
|
||||
// r.Env.Dimensions of repo.Directory.
|
||||
//
|
||||
// In the second step it collects the requested nodes.
|
||||
//
|
||||
// those two steps are independent.
|
||||
func (repo *Repo) GetContent(r *requests.Content) (c *content.SiteContent, err error) {
|
||||
// add more input validation
|
||||
err = repo.validateContentRequest(r)
|
||||
if err != nil {
|
||||
log.Debug("repo.GetContent invalid request", err)
|
||||
return
|
||||
}
|
||||
log.Debug("repo.GetContent: ", r.URI)
|
||||
c = content.NewSiteContent()
|
||||
resolved, resolvedURI, resolvedDimension, node := repo.resolveContent(r.Env.Dimensions, r.URI)
|
||||
if resolved {
|
||||
if !node.CanBeAccessedByGroups(r.Env.Groups) {
|
||||
log.Notice("401 for " + r.URI)
|
||||
c.Status = content.StatusForbidden
|
||||
} else {
|
||||
log.Notice("200 for " + r.URI)
|
||||
c.Status = content.StatusOk
|
||||
c.Data = node.Data
|
||||
}
|
||||
c.MimeType = node.MimeType
|
||||
c.Dimension = resolvedDimension
|
||||
c.URI = resolvedURI
|
||||
c.Item = node.ToItem([]string{})
|
||||
c.Path = node.GetPath()
|
||||
// fetch URIs for all dimensions
|
||||
uris := make(map[string]string)
|
||||
for dimensionName := range repo.Directory {
|
||||
uris[dimensionName] = repo.getURI(dimensionName, node.ID)
|
||||
}
|
||||
c.URIs = uris
|
||||
} else {
|
||||
log.Notice("404 for " + r.URI)
|
||||
c.Status = content.StatusNotFound
|
||||
c.Dimension = r.Env.Dimensions[0]
|
||||
}
|
||||
if log.SelectedLevel == log.LevelDebug {
|
||||
log.Debug(fmt.Sprintf("resolved: %v, uri: %v, dim: %v, n: %v", resolved, resolvedURI, resolvedDimension, node))
|
||||
}
|
||||
if resolved == false {
|
||||
log.Debug("repo.GetContent", r.URI, "could not be resolved falling back to default dimension", r.Env.Dimensions[0])
|
||||
// r.Env.Dimensions is validated => we can access it
|
||||
resolvedDimension = r.Env.Dimensions[0]
|
||||
}
|
||||
// add navigation trees
|
||||
for _, node := range r.Nodes {
|
||||
if node.Dimension == "" {
|
||||
node.Dimension = resolvedDimension
|
||||
}
|
||||
}
|
||||
c.Nodes = repo.getNodes(r.Nodes, r.Env)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetRepo get the whole repo in all dimensions
|
||||
func (repo *Repo) GetRepo() map[string]*content.RepoNode {
|
||||
response := make(map[string]*content.RepoNode)
|
||||
for dimensionName, dimension := range repo.Directory {
|
||||
response[dimensionName] = dimension.Node
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// Update - reload contents of repository with json from repo.server
|
||||
func (repo *Repo) Update() (updateResponse *responses.Update) {
|
||||
floatSeconds := func(nanoSeconds int64) float64 {
|
||||
return float64(float64(nanoSeconds) / float64(1000000000.0))
|
||||
}
|
||||
startTime := time.Now().UnixNano()
|
||||
updateRepotime, jsonBytes, updateErr := repo.update()
|
||||
updateResponse = &responses.Update{}
|
||||
updateResponse.Stats.RepoRuntime = floatSeconds(updateRepotime)
|
||||
|
||||
if updateErr != nil {
|
||||
updateResponse.Success = false
|
||||
updateResponse.Stats.NumberOfNodes = -1
|
||||
updateResponse.Stats.NumberOfURIs = -1
|
||||
// let us try to restore the world from a file
|
||||
log.Error("could not update repository:" + updateErr.Error())
|
||||
updateResponse.ErrorMessage = updateErr.Error()
|
||||
restoreErr := repo.tryToRestoreCurrent()
|
||||
if restoreErr != nil {
|
||||
log.Error("failed to restore preceding repo version: " + restoreErr.Error())
|
||||
} else {
|
||||
log.Record("restored current repo from local history")
|
||||
}
|
||||
} else {
|
||||
updateResponse.Success = true
|
||||
// persist the currently loaded one
|
||||
historyErr := repo.history.add(jsonBytes)
|
||||
if historyErr != nil {
|
||||
log.Warning("could not persist current repo in history: " + historyErr.Error())
|
||||
}
|
||||
// add some stats
|
||||
for dimension := range repo.Directory {
|
||||
updateResponse.Stats.NumberOfNodes += len(repo.Directory[dimension].Directory)
|
||||
updateResponse.Stats.NumberOfURIs += len(repo.Directory[dimension].URIDirectory)
|
||||
}
|
||||
}
|
||||
updateResponse.Stats.OwnRuntime = floatSeconds(time.Now().UnixNano()-startTime) - updateResponse.Stats.RepoRuntime
|
||||
return updateResponse
|
||||
}
|
||||
|
||||
// resolveContent find content in a repository
|
||||
func (repo *Repo) resolveContent(dimensions []string, URI string) (resolved bool, resolvedURI string, resolvedDimension string, repoNode *content.RepoNode) {
|
||||
parts := strings.Split(URI, content.PathSeparator)
|
||||
resolved = false
|
||||
resolvedURI = ""
|
||||
resolvedDimension = ""
|
||||
repoNode = nil
|
||||
log.Debug("repo.ResolveContent: " + URI)
|
||||
for i := len(parts); i > 0; i-- {
|
||||
testURI := strings.Join(parts[0:i], content.PathSeparator)
|
||||
if testURI == "" {
|
||||
testURI = content.PathSeparator
|
||||
}
|
||||
for _, dimension := range dimensions {
|
||||
if d, ok := repo.Directory[dimension]; ok {
|
||||
log.Debug(" testing[" + dimension + "]: " + testURI)
|
||||
if repoNode, ok := d.URIDirectory[testURI]; ok {
|
||||
resolved = true
|
||||
log.Debug(" found => " + testURI)
|
||||
log.Debug(" destination " + fmt.Sprint(repoNode.DestinationID))
|
||||
if len(repoNode.DestinationID) > 0 {
|
||||
if destionationNode, destinationNodeOk := d.Directory[repoNode.DestinationID]; destinationNodeOk {
|
||||
repoNode = destionationNode
|
||||
}
|
||||
}
|
||||
return true, testURI, dimension, repoNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const maxGetURIForNodeRecursionLevel = 1000
|
||||
|
||||
func (repo *Repo) getURIForNode(dimension string, repoNode *content.RepoNode, recursionLevel int64) (uri string) {
|
||||
if len(repoNode.LinkID) == 0 {
|
||||
uri = repoNode.URI
|
||||
return
|
||||
}
|
||||
linkedNode, ok := repo.Directory[dimension].Directory[repoNode.LinkID]
|
||||
if ok {
|
||||
if recursionLevel > maxGetURIForNodeRecursionLevel {
|
||||
log.Error("maxGetURIForNodeRecursionLevel reached for", repoNode.ID, "link id", repoNode.LinkID, "in dimension", dimension)
|
||||
return ""
|
||||
}
|
||||
return repo.getURIForNode(dimension, linkedNode, recursionLevel+1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (repo *Repo) getURI(dimension string, id string) string {
|
||||
repoNode, ok := repo.Directory[dimension].Directory[id]
|
||||
if ok {
|
||||
return repo.getURIForNode(dimension, repoNode, 0)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (repo *Repo) getNode(repoNode *content.RepoNode, expanded bool, mimeTypes []string, path []*content.Item, level int, groups []string, dataFields []string) *content.Node {
|
||||
node := content.NewNode()
|
||||
node.Item = repoNode.ToItem(dataFields)
|
||||
log.Debug("repo.GetNode: " + repoNode.ID)
|
||||
for _, childID := range repoNode.Index {
|
||||
childNode := repoNode.Nodes[childID]
|
||||
if (level == 0 || expanded || !expanded && childNode.InPath(path)) && !childNode.Hidden && childNode.CanBeAccessedByGroups(groups) && childNode.IsOneOfTheseMimeTypes(mimeTypes) {
|
||||
node.Nodes[childID] = repo.getNode(childNode, expanded, mimeTypes, path, level+1, groups, dataFields)
|
||||
node.Index = append(node.Index, childID)
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (repo *Repo) validateContentRequest(req *requests.Content) (err error) {
|
||||
if req == nil {
|
||||
return errors.New("request must not be nil")
|
||||
}
|
||||
if len(req.URI) == 0 {
|
||||
return errors.New("request URI must not be empty")
|
||||
}
|
||||
if req.Env == nil {
|
||||
return errors.New("request.Env must not be nil")
|
||||
}
|
||||
if len(req.Env.Dimensions) == 0 {
|
||||
return errors.New("request.Env.Dimensions must not be empty")
|
||||
}
|
||||
for _, envDimension := range req.Env.Dimensions {
|
||||
if !repo.hasDimension(envDimension) {
|
||||
availableDimensions := []string{}
|
||||
for availableDimension := range repo.Directory {
|
||||
availableDimensions = append(availableDimensions, availableDimension)
|
||||
}
|
||||
return fmt.Errorf("unknown dimension %q in r.Env must be one of %q", envDimension, availableDimensions)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repo) hasDimension(d string) bool {
|
||||
_, hasDimension := repo.Directory[d]
|
||||
return hasDimension
|
||||
}
|
||||
|
||||
func uriKeyForState(state string, uri string) string {
|
||||
return state + "-" + uri
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/foomo/contentserver/repo/mock"
|
||||
"github.com/foomo/contentserver/requests"
|
||||
)
|
||||
|
||||
func assertRepoIsEmpty(t *testing.T, r *Repo, empty bool) {
|
||||
if empty {
|
||||
if len(r.Directory) > 0 {
|
||||
t.Fatal("directory should have been empty, but is not")
|
||||
}
|
||||
} else {
|
||||
if len(r.Directory) == 0 {
|
||||
t.Fatal("directory should not have been empty, but it is")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad404(t *testing.T) {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-no-have"
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if response.Success {
|
||||
t.Fatal("can not get a repo, if the server responds with a 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBrokenRepo(t *testing.T) {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-broken-json.json"
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if response.Success {
|
||||
t.Fatal("how could we load a broken json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRepo(t *testing.T) {
|
||||
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-ok.json"
|
||||
r := NewRepo(server, varDir)
|
||||
assertRepoIsEmpty(t, r, true)
|
||||
response := r.Update()
|
||||
assertRepoIsEmpty(t, r, false)
|
||||
if response.Success == false {
|
||||
t.Fatal("could not load valid repo")
|
||||
}
|
||||
if response.Stats.OwnRuntime > response.Stats.RepoRuntime {
|
||||
t.Fatal("how could all take less time, than me alone")
|
||||
}
|
||||
if response.Stats.RepoRuntime < float64(0.05) {
|
||||
t.Fatal("the server was too fast")
|
||||
}
|
||||
|
||||
// see what happens if we try to start it up again
|
||||
nr := NewRepo(server, varDir)
|
||||
assertRepoIsEmpty(t, nr, false)
|
||||
}
|
||||
|
||||
func TestLoadRepoDuplicateUris(t *testing.T) {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-duplicate-uris.json"
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if response.Success {
|
||||
t.Fatal("there are duplicates, this repo update should have failed")
|
||||
}
|
||||
if !strings.Contains(response.ErrorMessage, "update dimension") {
|
||||
t.Fatal("error message not as expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDimensionHygiene(t *testing.T) {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-two-dimensions.json"
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if !response.Success {
|
||||
t.Fatal("well those two dimension should be fine")
|
||||
}
|
||||
r.server = mockServer.URL + "/repo-ok.json"
|
||||
response = r.Update()
|
||||
if !response.Success {
|
||||
t.Fatal("wtf it is called repo ok")
|
||||
}
|
||||
if len(r.Directory) != 1 {
|
||||
t.Fatal("directory hygiene failed")
|
||||
}
|
||||
}
|
||||
|
||||
func getTestRepo(path string, t *testing.T) *Repo {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + path
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if !response.Success {
|
||||
t.Fatal("well those two dimension should be fine")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestGetNodes(t *testing.T) {
|
||||
r := getTestRepo("/repo-two-dimensions.json", t)
|
||||
nodesRequest := mock.MakeNodesRequest()
|
||||
nodes := r.GetNodes(nodesRequest)
|
||||
testNode, ok := nodes["test"]
|
||||
if !ok {
|
||||
t.Fatal("wtf that should be a node")
|
||||
}
|
||||
testData, ok := testNode.Item.Data["foo"]
|
||||
t.Log("testData", testData)
|
||||
if !ok {
|
||||
t.Fatal("failed to fetch test data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveContent(t *testing.T) {
|
||||
r := getTestRepo("/repo-two-dimensions.json", t)
|
||||
|
||||
contentRequest := mock.MakeValidContentRequest()
|
||||
|
||||
siteContent, err := r.GetContent(contentRequest)
|
||||
if siteContent.URI != contentRequest.URI {
|
||||
t.Fatal("failed to resolve uri")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkIds(t *testing.T) {
|
||||
mockServer, varDir := mock.GetMockData(t)
|
||||
server := mockServer.URL + "/repo-link-ok.json"
|
||||
r := NewRepo(server, varDir)
|
||||
response := r.Update()
|
||||
if !response.Success {
|
||||
t.Fatal("those links should have been fine")
|
||||
}
|
||||
|
||||
r.server = mockServer.URL + "/repo-link-broken.json"
|
||||
response = r.Update()
|
||||
|
||||
if response.Success {
|
||||
t.Fatal("I do not think so")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInvalidRequest(t *testing.T) {
|
||||
|
||||
r := getTestRepo("/repo-two-dimensions.json", t)
|
||||
|
||||
if r.validateContentRequest(mock.MakeValidContentRequest()) != nil {
|
||||
t.Fatal("failed validation a valid request")
|
||||
}
|
||||
|
||||
tests := map[string]*requests.Content{}
|
||||
|
||||
rEmptyURI := mock.MakeValidContentRequest()
|
||||
rEmptyURI.URI = ""
|
||||
tests["empty uri"] = rEmptyURI
|
||||
|
||||
rEmptyEnv := mock.MakeValidContentRequest()
|
||||
rEmptyEnv.Env = nil
|
||||
tests["empty env"] = rEmptyEnv
|
||||
|
||||
rEmptyEnvDimensions := mock.MakeValidContentRequest()
|
||||
rEmptyEnvDimensions.Env.Dimensions = []string{}
|
||||
tests["empty env dimensions"] = rEmptyEnvDimensions
|
||||
|
||||
//rNodesValidID := mock.MakeValidContentRequest()
|
||||
//rNodesValidID.Nodes["id-root"].Id = ""
|
||||
//tests["nodes must have a valid id"] = rNodesValidID
|
||||
|
||||
for comment, req := range tests {
|
||||
if r.validateContentRequest(req) == nil {
|
||||
t.Fatal(comment, "should have failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
10
requests/content.go
Normal file
10
requests/content.go
Normal file
@ -0,0 +1,10 @@
|
||||
package requests
|
||||
|
||||
// Content - the standard request to contentserver
|
||||
type Content struct {
|
||||
Env *Env `json:"env"`
|
||||
URI string `json:"URI"`
|
||||
Nodes map[string]*Node `json:"nodes"`
|
||||
DataFields []string `json:"dataFields"`
|
||||
PathDataFields []string `json:"pathDataFields"`
|
||||
}
|
||||
9
requests/env.go
Normal file
9
requests/env.go
Normal file
@ -0,0 +1,9 @@
|
||||
package requests
|
||||
|
||||
// Env - abstract your server state
|
||||
type Env struct {
|
||||
// when resolving conten these are processed in their order
|
||||
Dimensions []string `json:"dimensions"`
|
||||
// who is it for
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
7
requests/itemmap.go
Normal file
7
requests/itemmap.go
Normal file
@ -0,0 +1,7 @@
|
||||
package requests
|
||||
|
||||
// ItemMap - map of items
|
||||
type ItemMap struct {
|
||||
ID string `json:"id"`
|
||||
DataFields []string `json:"dataFields"`
|
||||
}
|
||||
19
requests/node.go
Normal file
19
requests/node.go
Normal file
@ -0,0 +1,19 @@
|
||||
package requests
|
||||
|
||||
// Node - an abstract node request, use this one to request navigations
|
||||
type Node struct {
|
||||
// this one should be obvious
|
||||
ID string `json:"id"`
|
||||
// from which dimension
|
||||
Dimension string `json:"dimension"`
|
||||
// allowed access groups
|
||||
Groups []string `json:"groups"`
|
||||
// what do you want to see in your navigations, folders, images or unicorns
|
||||
MimeTypes []string `json:"mimeTypes"`
|
||||
// expand the navigation tree or just the path to the resolved content
|
||||
Expand bool `json:"expand"`
|
||||
// Expose hidden nodes
|
||||
ExposeHiddenNodes bool `json:"exposeHiddenNodes,omitempty"`
|
||||
// filter with these
|
||||
DataFields []string `json:"dataFields"`
|
||||
}
|
||||
8
requests/nodes.go
Normal file
8
requests/nodes.go
Normal file
@ -0,0 +1,8 @@
|
||||
package requests
|
||||
|
||||
// Nodes - which nodes in which dimensions
|
||||
type Nodes struct {
|
||||
// map[dimension]*node
|
||||
Nodes map[string]*Node `json:"nodes"`
|
||||
Env *Env `json:"env"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user