Merge pull request #31 from foomo/v1.11.x

Release v1.11.x
This commit is contained in:
Kevin Franklin Kim 2024-11-25 22:43:27 +01:00 committed by GitHub
commit 318173f848
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 3939 additions and 3104 deletions

12
.editorconfig Normal file
View 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
View 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

BIN
.github/assets/contentserver.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
- package-ecosystem: gomod
directory: '/'
schedule:
interval: weekly
- package-ecosystem: docker
directory: '/build'
schedule:
interval: weekly

37
.github/workflows/pr.yml vendored Normal file
View File

@ -0,0 +1,37 @@
name: PR check
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@v4
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
View 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@v5
with:
version: latest
args: release --clean --timeout=90m
env:
GITHUB_TOKEN: ${{ steps.app_token.outputs.token }}

21
.gitignore vendored
View File

@ -1,11 +1,14 @@
data
*.log
*.test
cprof-*
var
.*
*~
*.log
!.github/
!.husky/
!.editorconfig
!.gitignore
!.golangci.yml
!.goreleaser.yml
!.husky.yaml
!.yamllint.yaml
/bin/
/pkg/tmp/
/vendor
!.git*
/coverage.out
/coverage.html
/tmp/

133
.golangci.yml Normal file
View File

@ -0,0 +1,133 @@
run:
timeout: 5m
issues:
exclude-dirs:
- 'bin'
- 'tmp'
linters-settings:
gocritic:
disabled-checks:
- ifElseChain
- commentFormatting
revive:
rules:
- name: unused-parameter
disabled: true
linters:
disable-all: true
enable:
## Enabled by 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]
- gosimple # (megacheck) Linter for Go source code that specializes in simplifying code [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]
## Disabled by your configuration 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]
- 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]
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: true]
- goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode. [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]
- misspell # Finds commonly misspelled English words [fast: true, auto-fix: true]
- mirror # reports wrong mirror patterns of bytes/strings usage [fast: false, 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]
- 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]
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
- recvcheck # checks for receiver type consistency [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]
- stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false]
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [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]
- 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]
## Don't enable
#- 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]
#- 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]
#- gci # Gci controls Go package import order and makes it always deterministic. [fast: true, auto-fix: true]
#- 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 FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
#- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
#- interfacebloat # A linter that checks the number of methods inside an interface. [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]
#- mnd # An analyzer to detect magic numbers. [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]
#- 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]

118
.goreleaser.yml Normal file
View File

@ -0,0 +1,118 @@
project_name: contentserver
release:
github:
owner: foomo
name: contentserver
prerelease: auto
builds:
- binary: contentserver
goos:
- windows
- darwin
- linux
goarch:
- amd64
- arm64
goarm:
- '7'
env:
- CGO_ENABLED=0
main: ./main.go
flags:
- -trimpath
ldflags: -s -w -X github.com/foomo/contentserver/cmd.version={{.Version}}
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
- README.md
changelog:
use: github-native
brews:
# Repository to push the tap to.
- repository:
owner: foomo
name: homebrew-tap
caveats: "sesamy --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
View File

@ -0,0 +1,15 @@
hooks:
pre-commit:
- golangci-lint run --fast
- 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':
- goimports -l -w
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
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/commit-msg Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/fsmonitor-watchman Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/post-update Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-applypatch Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-commit Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-merge-commit Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-push Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-rebase Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/pre-receive Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/prepare-commit-msg Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/push-to-checkout Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/sendemail-validate Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

3
.husky/update Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
husky hook $(basename "$0") $*

View File

@ -1,9 +0,0 @@
language: go
go: "1.18"
os:
- linux
dist: trusty
sudo: false
install: true
script:
- make test

View File

@ -1,39 +0,0 @@
##############################
###### STAGE: BUILD ######
##############################
FROM golang:1.18 AS build-env
WORKDIR /src
COPY ./go.mod ./go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
go mod download -x
COPY ./ ./
RUN GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -trimpath -o /contentserver
##############################
###### STAGE: PACKAGE ######
##############################
FROM alpine
ENV CONTENT_SERVER_ADDR=0.0.0.0:80
ENV CONTENT_SERVER_VAR_DIR=/var/lib/contentserver
ENV LOG_JSON=1
RUN apk add --update --no-cache ca-certificates curl bash && rm -rf /var/cache/apk/*
COPY --from=build-env /contentserver /usr/sbin/contentserver
VOLUME $CONTENT_SERVER_VAR_DIR
ENTRYPOINT ["/usr/sbin/contentserver"]
CMD ["-address=$CONTENT_SERVER_ADDR", "-var-dir=$CONTENT_SERVER_VAR_DIR"]
EXPOSE 80
EXPOSE 9200

159
Makefile
View File

@ -1,92 +1,99 @@
SHELL := /bin/bash
.DEFAULT_GOAL:=help
-include .makerc
TAG?=latest
IMAGE=foomo/contentserver
# --- Targets -----------------------------------------------------------------
# Utils
# This allows us to accept extra arguments
%: .husky
@:
all: build test
tag:
echo $(TAG)
clean:
rm -fv bin/contentserve*
.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
# Build
## === Tasks ===
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` $(IMAGE):$(TAG)
echo "# tagged container `cat .image_id` as $(IMAGE):$(TAG)"
rm -vf .image_id .cacert.pem
build-testclient:
go build -o bin/testclient -i github.com/foomo/contentserver/testing/client
build-testserver:
go build -o bin/testserver -i github.com/foomo/contentserver/testing/server
package: build
pkg/build.sh
# Docker
docker-build:
DOCKER_BUILDKIT=1 docker build -t $(IMAGE):$(TAG) --platform linux/amd64 --progress=plain .
docker-push:
docker push $(IMAGE):$(TAG)
# Testing / benchmarks
.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 -v ./...
@GO_TEST_TAGS=-skip go test -coverprofile=coverage.out -race -json ./... | gotestfmt
bench:
go test -run=none -bench=. ./...
.PHONY: test.update
## Run tests and update snapshots
test.update:
@GO_TEST_TAGS=-skip go test -update -coverprofile=coverage.out -race -json ./... | gotestfmt
run-testserver:
bin/testserver -json-file var/cse-globus-stage-b-with-main-section.json
.PHONY: lint
## Run linter
lint:
@golangci-lint run
run-contentserver:
contentserver -var-dir var -webserver-address :9191 -address :9999 http://127.0.0.1:1234
.PHONY: lint.fix
## Fix lint violations
lint.fix:
@golangci-lint run --fix
run-contentserver-freeosmem:
contentserver -var-dir var -webserver-address :9191 -address :9999 -free-os-mem 1 http://127.0.0.1:1234
.PHONY: tidy
## Run go mod tidy
tidy:
@go mod tidy
run-prometheus:
prometheus --config.file=prometheus/prometheus.yml
.PHONY: outdated
## Show outdated direct dependencies
outdated:
@go list -u -m -json all | go-mod-outdated -update -direct
clean-var:
rm var/contentserver-repo-2019*
## Install binary
install:
@go build -o ${GOPATH}/bin/contentserver main.go
# Profiling
## Build binary
build:
@mkdir -p bin
@go build -o bin/contentserver main.go
test-cpu-profile:
go test -cpuprofile=cprof-client github.com/foomo/contentserver/client
go tool pprof --text client.test cprof-client
## === Utils ===
go test -cpuprofile=cprof-repo github.com/foomo/contentserver/repo
go tool pprof --text repo.test cprof-repo
test-gctrace:
GODEBUG=gctrace=1 go test ./...
test-malloctrace:
GODEBUG=allocfreetrace=1 go test ./...
trace:
curl http://localhost:6060/debug/pprof/trace?seconds=60 > cs-trace
go tool trace cs-trace
pprof-heap-web:
go tool pprof -http=":8081" http://localhost:6060/debug/pprof/heap
pprof-cpu-web:
go tool pprof -http=":8081" http://localhost:6060/debug/pprof/profile
.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)

View File

@ -1,51 +1,64 @@
[![Travis CI](https://travis-ci.org/foomo/contentserver.svg?branch=master)](https://travis-ci.org/foomo/contentserver)
[![Build Status](https://github.com/foomo/contentserver/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/foomo/contentserver/actions/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/foomo/contentserver)](https://goreportcard.com/report/github.com/foomo/contentserver)
[![Coverage Status](https://coveralls.io/repos/github/foomo/contentserver/badge.svg?branch=main&)](https://coveralls.io/github/foomo/contentserver?branch=main)
[![GoDoc](https://godoc.org/github.com/foomo/contentserver?status.svg)](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="graphics/Overview.svg" width="100%" height="500">
<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="graphics/Update-Flow.svg" width="100%" height="700">
<img src="docs/assets/Update-Flow.svg" width="100%" height="700">
### Usage
@ -70,6 +83,12 @@ Usage of contentserver:
path to export the webserver on - useful when behind a proxy (default "/contentserver")
```
## How to Contribute
Make a pull request...
## 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)_

14
build/buildx.Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM alpine:3.19.1
RUN apk --no-cache add ca-certificates
RUN addgroup -S contentserver && \
adduser -S -g contentserver 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"]

View File

@ -1,96 +1,58 @@
package client
import (
"context"
"errors"
"net/http"
"net/url"
"time"
"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"
)
var (
ErrEmptyServerURL = errors.New("empty contentserver url provided")
ErrInvalidServerURL = errors.New("invalid contentserver url provided")
)
// Client a content server client
type Client struct {
t transport
t Transport
}
func NewClient(
server string,
connectionPoolSize int,
waitTimeout time.Duration,
) (c *Client, err error) {
return NewClientWithTransport(NewSocketTransport(server, connectionPoolSize, waitTimeout))
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewClientWithTransport(
transport transport,
) (c *Client, err error) {
c = &Client{
func New(transport Transport) *Client {
return &Client{
t: transport,
}
return
}
var (
ErrEmptyServerURL = errors.New("empty contentserver url provided")
ErrInvalidServerURL = errors.New("invalid contentserver url provided")
)
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 != ""
}
// NewHTTPClient constructs a new client to talk to the contentserver.
// It returns an error if the provided url is empty or invalid.
func NewHTTPClient(server string) (c *Client, err error) {
if server == "" {
return nil, ErrEmptyServerURL
}
// validate url
if !isValidUrl(server) {
return nil, ErrInvalidServerURL
}
return NewHTTPClientWithTransport(NewHTTPTransport(server, http.DefaultClient))
}
func NewHTTPClientWithTransport(transport transport) (c *Client, err error) {
c = &Client{
t: transport,
}
return
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
// Update tell the server to update itself
func (c *Client) Update() (*responses.Update, error) {
func (c *Client) Update(ctx context.Context) (*responses.Update, error) {
type serverResponse struct {
Reply *responses.Update
}
resp := serverResponse{}
if err := c.t.call(server.HandlerUpdate, &requests.Update{}, &resp); err != nil {
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) (*content.SiteContent, error) {
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(server.HandlerGetContent, request, &resp); err != nil {
if err := c.t.Call(ctx, handler.RouteGetContent, request, &resp); err != nil {
return nil, err
}
@ -98,20 +60,20 @@ func (c *Client) GetContent(request *requests.Content) (*content.SiteContent, er
}
// GetURIs resolve uris for ids in a dimension
func (c *Client) GetURIs(dimension string, IDs []string) (map[string]string, error) {
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(server.HandlerGetURIs, &requests.URIs{Dimension: dimension, IDs: IDs}, &resp); err != nil {
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) (map[string]*content.Node, 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,
@ -120,24 +82,24 @@ func (c *Client) GetNodes(env *requests.Env, nodes map[string]*requests.Node) (m
Reply map[string]*content.Node
}
resp := serverResponse{}
if err := c.t.call(server.HandlerGetNodes, r, &resp); err != nil {
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() (map[string]*content.RepoNode, error) {
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(server.HandlerGetRepo, &requests.Repo{}, &resp); err != nil {
if err := c.t.Call(ctx, handler.RouteGetRepo, &requests.Repo{}, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
func (c *Client) ShutDown() {
c.t.shutdown()
func (c *Client) Close() {
c.t.Close()
}

View File

@ -1,210 +1,60 @@
package client
package client_test
import (
"net"
"strconv"
"context"
"sync"
"testing"
"time"
"github.com/foomo/contentserver/client"
"github.com/foomo/contentserver/content"
. "github.com/foomo/contentserver/logger"
"github.com/foomo/contentserver/repo/mock"
"github.com/foomo/contentserver/requests"
"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"
)
const pathContentserver = "/contentserver"
var (
testServerSocketAddr string
testServerWebserverAddr string
)
func init() {
SetupLogging(true, "contentserver_client_test.log")
}
func TestInvalidHTTPClientInit(t *testing.T) {
c, err := NewHTTPClient("")
assert.Nil(t, c)
assert.Error(t, err)
c, err = NewHTTPClient("bogus")
assert.Nil(t, c)
assert.Error(t, err)
c, err = NewHTTPClient("htt:/notaurl")
assert.Nil(t, c)
assert.Error(t, err)
c, err = NewHTTPClient("htts://notaurl")
assert.Nil(t, c)
assert.Error(t, err)
c, err = NewHTTPClient("/path/segment/only")
assert.Nil(t, c)
assert.Error(t, err)
}
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 getFreePort() int {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
panic(err)
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
panic(err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func getAvailableAddr() string {
return "127.0.0.1:" + strconv.Itoa(getFreePort())
}
func initTestServer(t testing.TB) (socketAddr, webserverAddr string) {
socketAddr = getAvailableAddr()
webserverAddr = getAvailableAddr()
testServer, varDir := mock.GetMockData(t)
go func() {
err := server.RunServerSocketAndWebServer(
testServer.URL+"/repo-two-dimensions.json",
socketAddr,
webserverAddr,
pathContentserver,
varDir,
server.DefaultRepositoryTimeout,
false,
)
if err != nil {
t.Fatal("test server crashed: ", err)
}
}()
socketClient, errClient := NewClient(socketAddr, 1, time.Duration(time.Millisecond*100))
if errClient != nil {
panic(errClient)
}
i := 0
for {
time.Sleep(time.Millisecond * 100)
r, err := socketClient.GetRepo()
if err != nil {
continue
}
if len(r) == 0 {
t.Fatal("received empty JSON from GetRepo")
}
if r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64) == float64(1) {
break
}
if i > 100 {
panic("this is taking too long")
}
i++
}
return
}
func getTestClients(t testing.TB) (socketClient *Client, httpClient *Client) {
if testServerSocketAddr == "" {
socketAddr, webserverAddr := initTestServer(t)
testServerSocketAddr = socketAddr
testServerWebserverAddr = webserverAddr
}
socketClient, errClient := NewClient(testServerSocketAddr, 25, time.Duration(time.Millisecond*100))
if errClient != nil {
t.Log(errClient)
t.Fail()
}
httpClient, errHTTPClient := NewHTTPClient("http://" + testServerWebserverAddr + pathContentserver)
if errHTTPClient != nil {
t.Log(errHTTPClient)
t.Fail()
}
return
}
func testWithClients(t *testing.T, testFunc func(c *Client)) {
socketClient, httpClient := getTestClients(t)
defer socketClient.ShutDown()
defer httpClient.ShutDown()
testFunc(socketClient)
testFunc(httpClient)
}
func TestUpdate(t *testing.T) {
testWithClients(t, func(c *Client) {
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()
response, err := c.Update(context.TODO())
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) {
testWithClients(t, func(c *Client) {
defer c.ShutDown()
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
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)
}
uriMap, err := c.GetURIs(context.TODO(), 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) {
testWithClients(t, func(c *Client) {
time.Sleep(time.Millisecond * 100)
r, err := c.GetRepo()
if err != nil {
t.Fatal(err)
}
if len(r) == 0 {
t.Fatal("received empty JSON from GetRepo")
}
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()
r, err := c.GetRepo(context.TODO())
require.NoError(t, err)
if assert.NotEmpty(t, r, "received empty JSON from GetRepo") {
assert.Equal(t, 1.0, r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64), "failed to drill deep for data") //nolint:all
}
})
}
func TestGetNodes(t *testing.T) {
testWithClients(t, func(c *Client) {
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
nodesRequest := mock.MakeNodesRequest()
nodes, err := c.GetNodes(nodesRequest.Env, nodesRequest.Nodes)
if err != nil {
t.Fatal(err)
}
nodes, err := c.GetNodes(context.TODO(), nodesRequest.Env, nodesRequest.Nodes)
require.NoError(t, err)
testNode, ok := nodes["test"]
if !ok {
t.Fatal("that should be a node")
@ -220,34 +70,18 @@ func TestGetNodes(t *testing.T) {
}
func TestGetContent(t *testing.T) {
testWithClients(t, func(c *Client) {
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
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")
}
response, err := c.GetContent(context.TODO(), request)
require.NoError(t, err)
assert.Equal(t, request.URI, response.URI)
assert.Equal(t, content.StatusOk, response.Status)
})
}
func BenchmarkSocketClientAndServerGetContent(b *testing.B) {
socketClient, _ := getTestClients(b)
benchmarkServerAndClientGetContent(b, 30, 100, socketClient)
}
func BenchmarkWebClientAndServerGetContent(b *testing.B) {
_, httpClient := getTestClients(b)
benchmarkServerAndClientGetContent(b, 30, 100, httpClient)
}
func benchmarkServerAndClientGetContent(b *testing.B, numGroups, numCalls int, client GetContentClient) {
b.Helper()
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
@ -258,30 +92,72 @@ func benchmarkServerAndClientGetContent(b *testing.B, numGroups, numCalls int, c
}
}
type GetContentClient interface {
GetContent(request *requests.Content) (response *content.SiteContent, err error)
}
func benchmarkClientAndServerGetContent(b testing.TB, numGroups, numCalls int, client GetContentClient) {
func benchmarkClientAndServerGetContent(tb testing.TB, numGroups, numCalls int, client GetContentClient) {
tb.Helper()
var wg sync.WaitGroup
wg.Add(numGroups)
for group := 0; group < numGroups; group++ {
go func(g int) {
go func() {
defer wg.Done()
request := mock.MakeValidContentRequest()
for i := 0; i < numCalls; i++ {
response, err := client.GetContent(request)
response, err := client.GetContent(context.TODO(), request)
if err == nil {
if request.URI != response.URI {
b.Fatal("uri mismatch")
tb.Fatal("uri mismatch")
}
if response.Status != content.StatusOk {
b.Fatal("unexpected status")
tb.Fatal("unexpected status")
}
}
}
}(group)
}()
}
// Wait for all HTTP fetches to complete.
wg.Wait()
}
func testWithClients(t *testing.T, testFunc func(t *testing.T, c *client.Client)) {
t.Helper()
l := zaptest.NewLogger(t)
httpRepoServer := initHTTPRepoServer(t, l)
socketRepoServer := initSocketRepoServer(t, l)
httpClient := newHTTPClient(t, httpRepoServer)
socketClient := newSocketClient(t, socketRepoServer.Addr().String())
defer func() {
httpRepoServer.Close()
socketRepoServer.Close()
httpClient.Close()
socketClient.Close()
}()
testFunc(t, httpClient)
testFunc(t, socketClient)
}
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(context.TODO()) //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))
// }

View File

@ -6,16 +6,16 @@ import (
)
type connectionPool struct {
server string
url string
// conn net.Conn
chanConnGet chan chan net.Conn
chanConnReturn chan connReturn
chanDrainPool chan int
}
func newConnectionPool(server string, connectionPoolSize int, waitTimeout time.Duration) *connectionPool {
func newConnectionPool(url string, connectionPoolSize int, waitTimeout time.Duration) *connectionPool {
connPool := &connectionPool{
server: server,
url: url,
chanConnGet: make(chan chan net.Conn),
chanConnReturn: make(chan connReturn),
chanDrainPool: make(chan int),
@ -92,7 +92,7 @@ RunLoop:
// refill connection pool
for _, poolEntry := range connectionPool {
if poolEntry.conn == nil {
newConn, errDial := net.Dial("tcp", c.server)
newConn, errDial := net.Dial("tcp", c.url)
poolEntry.err = errDial
poolEntry.conn = newConn
}
@ -126,10 +126,9 @@ RunLoop:
for _, i := range waitPoolLoosers {
delete(waitPool, i)
}
}
c.chanDrainPool = nil
c.chanConnReturn = nil
c.chanConnGet = nil
//fmt.Println("runloop is done", waitPool)
// fmt.Println("runloop is done", waitPool)
}

View File

@ -2,45 +2,86 @@ package client
import (
"bytes"
"context"
"errors"
"io/ioutil"
"io"
"net/http"
"github.com/foomo/contentserver/server"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/pkg/utils"
)
type httpTransport struct {
client *http.Client
endpoint string
}
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, client *http.Client) transport {
return &httpTransport{
endpoint: server,
client: client,
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
}
}
func (ht *httpTransport) shutdown() {
// nothing to do here
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (ht *httpTransport) call(handler server.Handler, request interface{}, response interface{}) error {
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.NewRequest(
req, errNewRequest := http.NewRequestWithContext(
ctx,
http.MethodPost,
ht.endpoint+"/"+string(handler),
t.endpoint+"/"+string(route),
bytes.NewBuffer(requestBytes),
)
if errNewRequest != nil {
return errNewRequest
}
httpResponse, errDo := ht.client.Do(req)
httpResponse, errDo := t.httpClient.Do(req)
if errDo != nil {
return errDo
}
@ -52,9 +93,13 @@ func (ht *httpTransport) call(handler server.Handler, request interface{}, respo
if httpResponse.Body == nil {
return errors.New("empty response body")
}
responseBytes, errRead := ioutil.ReadAll(httpResponse.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
}

View 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
View File

@ -0,0 +1,7 @@
package client
import (
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary

View File

@ -1,6 +1,7 @@
package client
import (
"context"
"errors"
"fmt"
"io"
@ -8,36 +9,35 @@ import (
"strconv"
"time"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/responses"
"github.com/foomo/contentserver/server"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type connReturn struct {
conn net.Conn
err error
}
type socketTransport struct {
type SocketTransport struct {
connPool *connectionPool
}
func NewSocketTransport(server string, connectionPoolSize int, waitTimeout time.Duration) transport {
return &socketTransport{
connPool: newConnectionPool(server, connectionPoolSize, waitTimeout),
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewSocketTransport(url string, connectionPoolSize int, waitTimeout time.Duration) *SocketTransport {
return &SocketTransport{
connPool: newConnectionPool(url, connectionPoolSize, waitTimeout),
}
}
func (st *socketTransport) shutdown() {
if st.connPool.chanDrainPool != nil {
st.connPool.chanDrainPool <- 1
}
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (c *socketTransport) call(handler server.Handler, request interface{}, response interface{}) error {
if c.connPool.chanDrainPool == nil {
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)
@ -45,19 +45,19 @@ func (c *socketTransport) call(handler server.Handler, request interface{}, resp
return fmt.Errorf("could not marshal request : %q", err)
}
netChan := make(chan net.Conn)
c.connPool.chanConnGet <- netChan
t.connPool.chanConnGet <- netChan
conn := <-netChan
if conn == nil {
return errors.New("could not get a connection")
}
returnConn := func(err error) {
c.connPool.chanConnReturn <- connReturn{
t.connPool.chanConnReturn <- connReturn{
conn: conn,
err: err,
}
}
// write header result will be like handler:2{}
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", handler, len(jsonBytes))), jsonBytes...)
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", route, len(jsonBytes))), jsonBytes...)
// send request
var (
@ -83,7 +83,7 @@ func (c *socketTransport) call(handler server.Handler, request interface{}, resp
n, err := conn.Read(buf)
if err != nil && err != io.EOF {
returnConn(err)
return fmt.Errorf("an error occured while reading the response: %q", err)
return fmt.Errorf("an error occurred while reading the response: %q", err)
}
if n == 0 {
break
@ -125,3 +125,9 @@ func (c *socketTransport) call(handler server.Handler, request interface{}, resp
returnConn(nil)
return nil
}
func (t *SocketTransport) Close() {
if t.connPool.chanDrainPool != nil {
t.connPool.chanDrainPool <- 1
}
}

View File

@ -0,0 +1,63 @@
package client_test
import (
"net"
"testing"
"time"
"github.com/foomo/contentserver/client"
"github.com/foomo/contentserver/pkg/handler"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"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) {
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)
assert.NoError(tb, conn.Close())
}()
}
}()
return ln
}

View File

@ -1,8 +1,12 @@
package client
import "github.com/foomo/contentserver/server"
import (
"context"
type transport interface {
call(handler server.Handler, request interface{}, response interface{}) error
shutdown()
"github.com/foomo/contentserver/pkg/handler"
)
type Transport interface {
Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error
Close()
}

136
cmd/flags.go Normal file
View File

@ -0,0 +1,136 @@
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 gracefulTimeoutFlag(v *viper.Viper) time.Duration {
return v.GetDuration("graceful.timeout")
}
func addGracefulTimeoutFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Duration("graceful-timeout", 0, "Graceful shutdown timeout to wait")
_ = v.BindPFlag("graceful.timeout", flags.Lookup("graceful-timeout"))
_ = v.BindEnv("graceful.timeout", "CONTENT_SERVER_GRACEFUL_TIMEOUT")
}
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")
}

102
cmd/http.go Normal file
View File

@ -0,0 +1,102 @@
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.WithHTTPReadmeService(true),
keel.WithHTTPPrometheusService(servicePrometheusEnabledFlag(v)),
keel.WithHTTPHealthzService(serviceHealthzEnabledFlag(v)),
keel.WithPrometheusMeter(servicePrometheusEnabledFlag(v)),
keel.WithOTLPGRPCTracer(otelEnabledFlag(v)),
keel.WithGracefulPeriod(gracefulTimeoutFlag(v)),
keel.WithGracefulPeriod(gracefulPeriodFlag(v)),
)
l := svr.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.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, "repo", func(ctx context.Context, l *zap.Logger) error {
return r.Start(ctx)
}),
service.NewHTTP(l, "http", addressFlag(v),
handler.NewHTTP(l, r, handler.WithBasePath(basePathFlag(v))),
middleware.Telemetry(),
middleware.Logger(),
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)
addGracefulTimeoutFlag(flags, v)
addShutdownTimeoutFlag(flags, v)
addOtelEnabledFlag(flags, v)
addServiceHealthzEnabledFlag(flags, v)
addServicePrometheusEnabledFlag(flags, v)
return cmd
}

57
cmd/root.go Normal file
View 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
View 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
View 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
}

View File

@ -25,23 +25,23 @@ type RepoNode struct {
// // NewRepoNode constructor
// func NewRepoNode() *RepoNode {
// return &RepoNode{
// Data: make(map[string]interface{}, 0), // set initial size to zero explicitely?
// 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,17 +51,16 @@ func (node *RepoNode) InPath(path []*Item) bool {
}
// GetPath get a path for a repo node
func (node *RepoNode) GetPath(dataFields []string) []*Item {
func (n *RepoNode) GetPath(dataFields []string) []*Item {
var (
parentNode = node.parent
parentNode = n.parent
pathLength = 0
)
for parentNode != nil {
parentNode = parentNode.parent
pathLength++
}
parentNode = node.parent
parentNode = n.parent
var (
i = 0
@ -81,19 +80,19 @@ func (node *RepoNode) GetPath(dataFields []string) []*Item {
}
// ToItem convert a repo node to a simple repo item
func (node *RepoNode) ToItem(dataFields []string) *Item {
func (n *RepoNode) ToItem(dataFields []string) *Item {
item := NewItem()
item.ID = node.ID
item.Name = node.Name
item.MimeType = node.MimeType
item.Hidden = node.Hidden
item.URI = node.URI
item.Groups = node.Groups
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 = node.Data
item.Data = n.Data
} else {
for _, dataField := range dataFields {
if data, ok := node.Data[dataField]; ok {
if data, ok := n.Data[dataField]; ok {
item.Data[dataField] = data
}
}
@ -102,23 +101,23 @@ func (node *RepoNode) ToItem(dataFields []string) *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
}
}
@ -127,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
}
@ -144,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)
}
}

View File

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

View File

@ -1,111 +0,0 @@
package main
import (
"flag"
"fmt"
_ "net/http/pprof"
"os"
"runtime/debug"
"time"
. "github.com/foomo/contentserver/logger"
"github.com/foomo/contentserver/metrics"
"github.com/foomo/contentserver/server"
"github.com/foomo/contentserver/status"
"go.uber.org/zap"
)
const (
ServiceName = "Content Server"
DefaultHealthzHandlerAddress = ":8080"
DefaultPrometheusListener = ":9200"
)
var (
flagAddress = flag.String("address", "", "address to bind socket server host:port")
flagWebserverAddress = flag.String("webserver-address", "", "address to bind web server host:port, when empty no webserver will be spawned")
flagWebserverPath = flag.String("webserver-path", "/contentserver", "path to export the webserver on - useful when behind a proxy")
flagVarDir = flag.String("var-dir", "/var/lib/contentserver", "where to put my data")
flagPrometheusListener = flag.String("prometheus-listener", getenv("PROMETHEUS_LISTENER", DefaultPrometheusListener), "address for the prometheus listener")
flagRepositoryTimeoutDuration = flag.Duration("repository-timeout-duration", server.DefaultRepositoryTimeout, "timeout duration for the contentserver")
flagPoll = flag.Bool("poll", false, "if true, the address arg will be used to periodically poll the content url")
// debugging / profiling
flagDebug = flag.Bool("debug", false, "toggle debug mode")
flagFreeOSMem = flag.Int("free-os-mem", 0, "free OS mem every X minutes")
flagHeapDump = flag.Int("heap-dump", 0, "dump heap every X minutes")
)
func getenv(env, fallback string) string {
if value, ok := os.LookupEnv(env); ok {
return value
}
return fallback
}
func exitUsage(code int) {
fmt.Println("Usage:", os.Args[0], "http(s)://your-content-server/path/to/content.json")
flag.PrintDefaults()
os.Exit(code)
}
func main() {
flag.Parse()
SetupLogging(*flagDebug, "contentserver.log")
if *flagFreeOSMem > 0 {
Log.Info("freeing OS memory every $interval minutes", zap.Int("interval", *flagFreeOSMem))
go func() {
for {
time.Sleep(time.Duration(*flagFreeOSMem) * time.Minute)
Log.Info("FreeOSMemory")
debug.FreeOSMemory()
}
}()
}
if *flagHeapDump > 0 {
Log.Info("dumping heap every $interval minutes", zap.Int("interval", *flagHeapDump))
go func() {
for {
time.Sleep(time.Duration(*flagFreeOSMem) * time.Minute)
Log.Info("HeapDump")
f, err := os.Create("heapdump")
if err != nil {
panic("failed to create heap dump file")
}
debug.WriteHeapDump(f.Fd())
err = f.Close()
if err != nil {
panic("failed to create heap dump file")
}
}
}()
}
if len(flag.Args()) == 1 {
fmt.Println(*flagAddress, flag.Arg(0))
// kickoff metric handlers
go metrics.RunPrometheusHandler(*flagPrometheusListener)
go status.RunHealthzHandlerListener(DefaultHealthzHandlerAddress, ServiceName)
err := server.RunServerSocketAndWebServer(
flag.Arg(0),
*flagAddress,
*flagWebserverAddress,
*flagWebserverPath,
*flagVarDir,
*flagRepositoryTimeoutDuration,
*flagPoll,
)
if err != nil {
fmt.Println("exiting with error", err)
os.Exit(1)
}
} else {
exitUsage(1)
}
}

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

145
go.mod
View File

@ -1,32 +1,133 @@
module github.com/foomo/contentserver
go 1.23.0
require (
github.com/google/uuid v1.3.0
github.com/foomo/keel v0.19.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.12.1
github.com/stretchr/testify v1.7.0
go.uber.org/multierr v1.8.0
go.uber.org/zap v1.21.0
github.com/prometheus/client_golang v1.20.5
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.9.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.34.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
github.com/spf13/pflag v1.0.5
golang.org/x/net v0.31.0
)
go 1.18
require (
cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/avast/retry-go/v4 v4.6.0 // 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.0 // 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.1 // indirect
github.com/fatih/color v1.14.1 // 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.7.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/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
github.com/hashicorp/consul/api v1.28.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // 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.37.0 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // 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.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/sagikazarmark/crypt v0.19.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.24.10 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.2.4 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.12 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect
go.etcd.io/etcd/client/v2 v2.305.12 // indirect
go.etcd.io/etcd/client/v3 v3.5.12 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/host v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.171.0 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

705
go.sum
View File

@ -1,158 +1,229 @@
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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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/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/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
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/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.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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-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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/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/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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/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/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/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-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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/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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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.0/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.4.1/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.1/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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/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.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
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/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/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/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/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/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-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-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.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
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/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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
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.9/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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
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/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.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/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-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/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=
@ -160,316 +231,276 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
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/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
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_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 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
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/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
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.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE=
github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
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/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.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.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
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/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/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
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/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/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/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/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.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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/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/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
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/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/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/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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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/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/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/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE=
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M=
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/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/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/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/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/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/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/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/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/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
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/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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/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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/mod v0.4.2/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
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/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.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201020160332-67f06af15bc9/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/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/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-20200803210538-64077c9b5642/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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU=
google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o=
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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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/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/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/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
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/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=
@ -478,36 +509,24 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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.2.8/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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -1,43 +0,0 @@
package logger
import (
"log"
"os"
"go.uber.org/zap"
)
var (
// Log is the logger instance exposed by this package
// call Setup() prior to using it
// want JSON output? Set LOG_JSON env var to 1!
Log *zap.Logger
)
// SetupLogging configures the logger
func SetupLogging(debug bool, outputPath string) {
var (
zc zap.Config
err error
)
if debug {
zc = zap.NewDevelopmentConfig()
zc.OutputPaths = append(zc.OutputPaths, outputPath)
} else {
zc = zap.NewProductionConfig()
zc.OutputPaths = append(zc.OutputPaths, outputPath)
}
if os.Getenv("LOG_JSON") == "1" {
zc.Encoding = "json"
} else {
zc.Encoding = "console"
}
Log, err = zc.Build()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
}

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"github.com/foomo/contentserver/cmd"
)
func main() {
cmd.Execute()
}

View File

@ -1,21 +0,0 @@
package metrics
import (
"net/http"
. "github.com/foomo/contentserver/logger"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
const metricsRoute = "/metrics"
func RunPrometheusHandler(listener string) {
Log.Info("starting prometheus handler on",
zap.String("address", listener),
zap.String("route", metricsRoute),
)
Log.Error("prometheus listener failed",
zap.Error(http.ListenAndServe(listener, promhttp.Handler())),
)
}

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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 = ""
i = 0
)
for {
i++
// fmt.Println("---->", i)
// let us read with 1 byte steps on conn until we find "{"
_, readErr := conn.Read(headerBuffer[0:])
if readErr != nil {
h.l.Debug("looks like the client closed the connection", zap.Error(readErr))
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
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
View 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
View 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
}

170
pkg/repo/history.go Normal file
View File

@ -0,0 +1,170 @@
package repo
import (
"bytes"
"fmt"
"io"
"os"
"path"
"sort"
"strings"
"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
}
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 {
var filename = path.Join(h.historyDir, HistoryRepoJSONPrefix+time.Now().Format(time.RFC3339Nano)+HistoryRepoJSONSuffix)
if err := os.MkdirAll(path.Dir(filename), 0700); err != nil {
return errors.Wrap(err, "failed to create history dir")
}
if err := os.WriteFile(filename, jsonBytes, 0600); err != nil {
return errors.Wrap(err, "failed to write history")
}
h.l.Debug("adding content backup", zap.String("file", filename))
// current filename
if err := os.WriteFile(h.GetCurrentFilename(), 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 {
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
}

75
pkg/repo/history_test.go Normal file
View File

@ -0,0 +1,75 @@
package repo
import (
"bytes"
"fmt"
"os"
"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)
tempDir, err := os.MkdirTemp(os.TempDir(), "contentserver-history-test")
require.NoError(t, err)
return NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(tempDir))
}

385
pkg/repo/loader.go Normal file
View File

@ -0,0 +1,385 @@
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", zap.Error(ctx.Err()))
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", zap.Error(ctx.Err()))
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", zap.Error(ctx.Err()))
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
}
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
}

View File

@ -1,32 +1,33 @@
package mock
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"runtime"
"testing"
"time"
"github.com/foomo/contentserver/requests"
"github.com/stretchr/testify/require"
)
// GetMockData mock data to run a repo
func GetMockData(t testing.TB) (server *httptest.Server, varDir string) {
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)
}
varDir, err := os.MkdirTemp("", "content-server-test")
require.NoError(tb, err)
return server, varDir
}
@ -37,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{},
@ -66,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"},
@ -75,5 +76,4 @@ func MakeValidContentRequest() *requests.Content {
},
},
}
}

512
pkg/repo/repo.go Normal file
View File

@ -0,0 +1,512 @@
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) {
f, err := os.Open(r.history.GetCurrentFilename())
if err != nil {
r.l.Error("Failed to open Repo JSON", zap.Error(err))
}
_, _ = w.Write([]byte("{\"reply\":"))
_, err = io.Copy(w, f)
if err != nil {
r.l.Error("Failed to serve Repo JSON", zap.Error(err))
}
_, _ = w.Write([]byte("}"))
}
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()
}
// 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
}

View File

@ -1,39 +1,34 @@
package repo
import (
"strings"
"context"
"testing"
"time"
. "github.com/foomo/contentserver/logger"
_ "github.com/foomo/contentserver/logger"
"github.com/foomo/contentserver/repo/mock"
"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 init() {
SetupLogging(true, "contentserver_repo_test.log")
}
func NewTestRepo(server, varDir string) *Repo {
r := NewRepo(server, varDir, 2*time.Minute, false)
// because the travis CI VMs are very slow,
// we need to add some delay to allow the server to startup
time.Sleep(1 * time.Second)
func NewTestRepo(l *zap.Logger, url, varDir string) *Repo {
h := NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(varDir))
r := New(l, url, h)
go r.Start(context.Background()) //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 {
if len(r.Directory()) > 0 {
t.Fatal("directory should have been empty, but is not")
}
} else {
if len(r.Directory) == 0 {
if len(r.Directory()) == 0 {
t.Fatal("directory is empty, but should have been not")
}
}
@ -41,9 +36,10 @@ func assertRepoIsEmpty(t *testing.T, r *Repo, empty bool) {
func TestLoad404(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-no-have"
r = NewTestRepo(server, varDir)
url = mockServer.URL + "/repo-no-have"
r = NewTestRepo(l, url, varDir)
)
response := r.Update()
@ -54,9 +50,10 @@ func TestLoad404(t *testing.T) {
func TestLoadBrokenRepo(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-broken-json.json"
r = NewTestRepo(server, varDir)
r = NewTestRepo(l, server, varDir)
)
response := r.Update()
@ -66,13 +63,13 @@ func TestLoadBrokenRepo(t *testing.T) {
}
func TestLoadRepo(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-ok.json"
r = NewTestRepo(server, varDir)
r = NewTestRepo(l, server, varDir)
)
assertRepoIsEmpty(t, r, true)
assertRepoIsEmpty(t, r, false)
response := r.Update()
assertRepoIsEmpty(t, r, false)
@ -83,32 +80,29 @@ func TestLoadRepo(t *testing.T) {
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) {
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(server, varDir)
assertRepoIsEmpty(t, nr, false)
// 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(server, varDir)
r = NewTestRepo(l, server, varDir)
)
if len(r.Directory) > 0 {
b.Fatal("directory should have been empty, but is not")
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
response := r.Update()
if len(r.Directory) == 0 {
if len(r.Directory()) == 0 {
b.Fatal("directory is empty, but should have been not")
}
@ -119,114 +113,94 @@ func BenchmarkLoadRepo(b *testing.B) {
}
func TestLoadRepoDuplicateUris(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-duplicate-uris.json"
r = NewTestRepo(server, varDir)
r = NewTestRepo(l, 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: " + response.ErrorMessage)
}
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)
var (
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-two-dimensions.json"
r = NewTestRepo(server, varDir)
)
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-two-dimensions.json"
r := NewTestRepo(l, server, varDir)
response := r.Update()
if !response.Success {
t.Fatal("well those two dimension should be fine")
}
r.server = mockServer.URL + "/repo-ok.json"
require.True(t, response.Success, "well those two dimension should be fine")
r.url = 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")
}
require.True(t, response.Success, "it is called repo ok")
assert.Lenf(t, r.Directory(), 1, "directory hygiene failed")
}
func getTestRepo(path string, t *testing.T) *Repo {
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(l, server, varDir)
response := r.Update()
require.True(t, response.Success, "well those two dimension should be fine")
var (
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + path
r = NewTestRepo(server, varDir)
response = r.Update()
)
if !response.Success {
t.Fatal("well those two dimension should be fine")
}
return r
}
func TestGetNodes(t *testing.T) {
var (
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")
}
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)
if !ok {
t.Fatal("failed to fetch test data")
}
}
func TestGetNodesExposeHidden(t *testing.T) {
var (
r = getTestRepo("/repo-ok-exposehidden.json", t)
nodesRequest = mock.MakeNodesRequest()
)
r := getTestRepo(t, "/repo-ok-exposehidden.json")
nodesRequest := mock.MakeNodesRequest()
nodesRequest.Nodes["test"].ExposeHiddenNodes = true
nodes := r.GetNodes(nodesRequest)
testNode, ok := nodes["test"]
if !ok {
t.Fatal("wtf that should be a node")
}
require.True(t, ok, "should be a node")
_, ok = testNode.Item.Data["foo"]
if !ok {
t.Fatal("failed to fetch test data")
}
require.Equal(t, 2, len(testNode.Nodes))
require.True(t, ok, "failed to fetch test data")
require.Len(t, testNode.Nodes, 2)
}
func TestResolveContent(t *testing.T) {
var (
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)
}
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(server, varDir)
r = NewTestRepo(l, server, varDir)
response = r.Update()
)
@ -234,18 +208,16 @@ func TestLinkIds(t *testing.T) {
t.Fatal("those links should have been fine")
}
r.server = mockServer.URL + "/repo-link-broken.json"
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("/repo-two-dimensions.json", t)
r := getTestRepo(t, "/repo-two-dimensions.json")
if r.validateContentRequest(mock.MakeValidContentRequest()) != nil {
t.Fatal("failed validation a valid request")
@ -265,9 +237,9 @@ func TestInvalidRequest(t *testing.T) {
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
// 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 {

10
pkg/repo/repodimension.go Normal file
View 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
View 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 != ""
}

View File

@ -1,14 +0,0 @@
# reference: https://prometheus.io/docs/prometheus/latest/configuration/configuration/
global:
scrape_interval: 15s
scrape_timeout: 15s
#evaluation_interval: 15s
scrape_configs:
- job_name: contentserver
metrics_path: /metrics
scheme: http
static_configs:
- targets:
- 127.0.0.1:9111

View File

@ -1,137 +0,0 @@
package repo
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"sort"
"strings"
"time"
. "github.com/foomo/contentserver/logger"
"go.uber.org/zap"
)
const (
historyRepoJSONPrefix = "contentserver-repo-"
historyRepoJSONSuffix = ".json"
)
var flagMaxHistoryVersions = flag.Int("max-history", 2, "set the maximum number of content backup files")
type history struct {
varDir string
}
func newHistory(varDir string) *history {
return &history{
varDir: varDir,
}
}
func (h *history) add(jsonBytes []byte) error {
var (
// historiy 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
}
Log.Info("adding content backup", zap.String("file", filename))
// current filename
err = ioutil.WriteFile(h.getCurrentFilename(), jsonBytes, 0644)
if err != nil {
return err
}
err = h.cleanup()
if err != nil {
Log.Error("an error occured while cleaning up my history", zap.Error(err))
return err
}
return nil
}
func (h *history) getHistory() (files []string, err error) {
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(*flagMaxHistoryVersions)
if err != nil {
return err
}
for _, f := range files {
Log.Info("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
}
func (h *history) getCurrentFilename() string {
return path.Join(h.varDir, historyRepoJSONPrefix+"current"+historyRepoJSONSuffix)
}
func (h *history) getCurrent(buf *bytes.Buffer) (err error) {
f, err := os.Open(h.getCurrentFilename())
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(buf, f)
return err
}

View File

@ -1,92 +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) {
var (
h = testHistory()
test = []byte("test")
b bytes.Buffer
)
err := h.add(test)
if err != nil {
t.Fatal("failed to add: ", err)
}
err = h.getCurrent(&b)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b.Bytes(), test) {
t.Fatalf("expected %q, got %q", string(test), b.String())
}
}
func TestHistoryCleanup(t *testing.T) {
h := testHistory()
for i := 0; i < 50; i++ {
err := h.add([]byte(fmt.Sprint(i)))
if err != nil {
t.Fatal("failed to add: ", err)
}
time.Sleep(time.Millisecond * 5)
}
err := h.cleanup()
if err != nil {
t.Fatal("failed to run cleanup: ", err)
}
files, err := h.getHistory()
if err != nil {
t.Fatal(err)
}
// -1 for ignoring the current content backup file
if len(files)-1 != *flagMaxHistoryVersions {
t.Fatal("history too long", len(files), "instead of", *flagMaxHistoryVersions)
}
}
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-21.json", files[0])
}
func assertStringEqual(t *testing.T, expected, actual string) {
if expected != actual {
t.Errorf("expected string %s differs from the actual %s", expected, actual)
}
}

View File

@ -1,333 +0,0 @@
package repo
import (
"context"
"io"
"io/ioutil"
"net/http"
"time"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/logger"
"github.com/foomo/contentserver/status"
"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 (repo *Repo) updateRoutine() {
for resChan := range repo.updateInProgressChannel {
log := logger.Log.With(zap.String("updateRunID", uuid.New().String()))
log.Info("Content update started")
start := time.Now()
repoRuntime, err := repo.update(context.Background())
if err != nil {
log.Error("Content update failed", zap.Error(err))
status.M.UpdatesFailedCounter.WithLabelValues().Inc()
} else {
log.Info("Content update success")
status.M.UpdatesCompletedCounter.WithLabelValues().Inc()
}
resChan <- updateResponse{
repoRuntime: repoRuntime,
err: err,
}
status.M.UpdateDuration.WithLabelValues().Observe(time.Since(start).Seconds())
}
}
func (repo *Repo) dimensionUpdateRoutine() {
for newDimension := range repo.dimensionUpdateChannel {
logger.Log.Info("dimensionUpdateRoutine received a new dimension", zap.String("dimension", newDimension.Dimension))
err := repo._updateDimension(newDimension.Dimension, newDimension.Node)
logger.Log.Info("dimensionUpdateRoutine received result")
if err != nil {
logger.Log.Debug("update dimension failed", zap.Error(err))
}
repo.dimensionUpdateDoneChannel <- err
}
}
func (repo *Repo) updateDimension(dimension string, node *content.RepoNode) error {
logger.Log.Debug("trying to push dimension into update channel", zap.String("dimension", dimension), zap.String("nodeName", node.Name))
repo.dimensionUpdateChannel <- &repoDimension{
Dimension: dimension,
Node: node,
}
logger.Log.Debug("waiting for done signal")
return <-repo.dimensionUpdateDoneChannel
}
// do not call directly, but only through channel
func (repo *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 repo.Directory {
if d != dimension {
newRepoDirectory[d] = D
}
}
// add the new dimension
newRepoDirectory[dimension] = &Dimension{
Node: newNode,
Directory: newDirectory,
URIDirectory: newURIDirectory,
}
repo.Directory = 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 {
// Log.Debug("buildDirectory", zap.String("ID", 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 := 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 (repo *Repo) loadNodesFromJSON() (nodes map[string]*content.RepoNode, err error) {
nodes = make(map[string]*content.RepoNode)
err = json.Unmarshal(repo.jsonBuf.Bytes(), &nodes)
if err != nil {
logger.Log.Error("Failed to deserialize nodes", zap.Error(err))
return nil, errors.New("failed to deserialize nodes")
}
return nodes, nil
}
func (repo *Repo) tryToRestoreCurrent() (err error) {
err = repo.history.getCurrent(&repo.jsonBuf)
if err != nil {
return err
}
return repo.loadJSONBytes()
}
func (repo *Repo) get(URL string) error {
response, err := repo.httpClient.Get(URL)
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)
repo.jsonBuf.Reset()
// Log.Info(ansi.Green + "LOADING DATA INTO BUFFER" + ansi.Reset)
_, err = io.Copy(&repo.jsonBuf, response.Body)
if err != nil {
return errors.Wrap(err, "failed to copy IO stream")
}
return nil
}
func (repo *Repo) update(ctx context.Context) (repoRuntime int64, err error) {
startTimeRepo := time.Now().UnixNano()
repoURL := repo.server
if repo.pollForUpdates {
resp, err := repo.httpClient.Get(repo.server)
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 := ioutil.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 == repo.pollVersion {
logger.Log.Info(
"repo is up to date",
zap.String("pollVersion", repo.pollVersion),
)
// already up to date
return repoRuntime, nil
} else {
logger.Log.Info(
"new repo poll version",
zap.String("pollVersion", repo.pollVersion),
)
}
}
err = repo.get(repoURL)
repoRuntime = time.Now().UnixNano() - startTimeRepo
if err != nil {
// we have no json to load - the repo server did not reply
logger.Log.Debug("Failed to load json", zap.Error(err))
return repoRuntime, err
}
logger.Log.Debug("loading json", zap.String("server", repoURL), zap.Int("length", len(repo.jsonBuf.Bytes())))
nodes, err := repo.loadNodesFromJSON()
if err != nil {
// could not load nodes from json
return repoRuntime, err
}
err = repo.loadNodes(nodes)
if err != nil {
// repo failed to load nodes
return repoRuntime, err
}
if repo.pollForUpdates {
repo.pollVersion = repoURL
}
return repoRuntime, nil
}
// limit ressources and allow only one update request at once
func (repo *Repo) tryUpdate() (repoRuntime int64, err error) {
c := make(chan updateResponse)
select {
case repo.updateInProgressChannel <- c:
logger.Log.Info("update request added to queue")
ur := <-c
return ur.repoRuntime, ur.err
default:
logger.Log.Info("update request accepted, will be processed after the previous update")
return 0, errUpdateRejected
}
}
func (repo *Repo) loadJSONBytes() error {
nodes, err := repo.loadNodesFromJSON()
if err != nil {
data := repo.jsonBuf.Bytes()
if len(data) > 10 {
logger.Log.Debug("could not parse json",
zap.String("jsonStart", string(data[:10])),
zap.String("jsonStart", string(data[len(data)-10:])),
)
}
return err
}
err = repo.loadNodes(nodes)
if err == nil {
errHistory := repo.history.add(repo.jsonBuf.Bytes())
if errHistory != nil {
logger.Log.Error("Could not add valid JSON to history", zap.Error(errHistory))
status.M.HistoryPersistFailedCounter.WithLabelValues().Inc()
} else {
logger.Log.Info("Added valid JSON to history")
}
}
return err
}
func (repo *Repo) loadNodes(newNodes map[string]*content.RepoNode) error {
var newDimensions []string
var err error
for dimension, newNode := range newNodes {
newDimensions = append(newDimensions, dimension)
logger.Log.Debug("Loading nodes for dimension", zap.String("dimension", dimension))
errLoad := repo.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
for dimension := range repo.Directory {
if !dimensionIsValid(dimension) {
logger.Log.Info("Removing orphaned dimension", zap.String("dimension", dimension))
delete(repo.Directory, dimension)
}
}
return nil
}

View File

@ -1,440 +0,0 @@
package repo
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/foomo/contentserver/status"
"go.uber.org/zap"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/logger"
"github.com/foomo/contentserver/requests"
"github.com/foomo/contentserver/responses"
)
const maxGetURIForNodeRecursionLevel = 1000
// 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 {
pollForUpdates bool
pollVersion string
server string
recovered bool
Directory map[string]*Dimension
// updateLock sync.Mutex
dimensionUpdateChannel chan *repoDimension
dimensionUpdateDoneChannel chan error
history *history
updateInProgressChannel chan chan updateResponse
// jsonBytes []byte
jsonBuf bytes.Buffer
httpClient *http.Client
}
type repoDimension struct {
Dimension string
Node *content.RepoNode
}
// NewRepo constructor
func NewRepo(server string, varDir string, repositoryTimeout time.Duration, pollForUpdates bool) *Repo {
logger.Log.Info("creating new repo",
zap.String("server", server),
zap.String("varDir", varDir),
)
repo := &Repo{
pollForUpdates: pollForUpdates,
recovered: false,
server: server,
Directory: map[string]*Dimension{},
history: newHistory(varDir),
dimensionUpdateChannel: make(chan *repoDimension),
dimensionUpdateDoneChannel: make(chan error),
httpClient: getDefaultHTTPClient(repositoryTimeout),
updateInProgressChannel: make(chan chan updateResponse),
}
go repo.updateRoutine()
go repo.dimensionUpdateRoutine()
if pollForUpdates {
go func() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
chanReponse := make(chan updateResponse)
repo.updateInProgressChannel <- chanReponse
response := <-chanReponse
if response.err == nil {
logger.Log.Info("poll update success", zap.String("revision", repo.pollVersion))
} else {
logger.Log.Info("poll error", zap.Error(response.err))
}
}
}()
}
logger.Log.Info("trying to restore previous state")
restoreErr := repo.tryToRestoreCurrent()
if restoreErr != nil {
logger.Log.Error(" could not restore previous repo content", zap.Error(restoreErr))
} else {
repo.recovered = true
logger.Log.Info("restored previous repo content")
}
return repo
}
func getDefaultHTTPClient(timeout time.Duration) *http.Client {
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: 5 * time.Second,
},
Timeout: timeout,
}
return client
}
func (repo *Repo) Recovered() bool {
return repo.recovered
}
// 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 {
var (
nodes = map[string]*content.Node{}
path = []*content.Item{}
)
for nodeName, nodeRequest := range nodeRequests {
if nodeName == "" || nodeRequest.ID == "" {
logger.Log.Info("invalid node request", zap.Error(errors.New("nodeName or nodeRequest.ID empty")))
continue
}
logger.Log.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 := repo.Directory[nodeRequest.Dimension]
nodes[nodeName] = nil
if !ok && nodeRequest.Dimension == "" {
logger.Log.Debug("Could not get dimension root node", zap.String("dimension", nodeRequest.Dimension))
for _, dimension := range env.Dimensions {
dimensionNode, ok = repo.Directory[dimension]
if ok {
logger.Log.Debug("Found root node in env.Dimensions", zap.String("dimension", dimension))
break
}
logger.Log.Debug("Could NOT find root node in env.Dimensions", zap.String("dimension", dimension))
}
}
if !ok {
logger.Log.Error("could not get dimension root node", zap.String("nodeRequest.Dimension", nodeRequest.Dimension))
continue
}
treeNode, ok := dimensionNode.Directory[nodeRequest.ID]
if !ok {
logger.Log.Error("Invalid tree node requested",
zap.String("nodeName", nodeName),
zap.String("nodeID", nodeRequest.ID),
)
status.M.InvalidNodeTreeRequests.WithLabelValues().Inc()
continue
}
nodes[nodeName] = repo.getNode(treeNode, nodeRequest.Expand, nodeRequest.MimeTypes, path, 0, groups, nodeRequest.DataFields, nodeRequest.ExposeHiddenNodes)
}
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 {
logger.Log.Error("repo.GetContent invalid request", zap.Error(err))
return
}
logger.Log.Debug("repo.GetContent", zap.String("URI", r.URI))
c = content.NewSiteContent()
resolved, resolvedURI, resolvedDimension, node := repo.resolveContent(r.Env.Dimensions, r.URI)
if resolved {
if !node.CanBeAccessedByGroups(r.Env.Groups) {
logger.Log.Warn("Resolved content cannot be accessed by specified group", zap.String("URI", r.URI))
c.Status = content.StatusForbidden
} else {
logger.Log.Info("Content resolved", zap.String("URI", r.URI))
c.Status = content.StatusOk
c.Data = node.Data
}
c.MimeType = node.MimeType
c.Dimension = resolvedDimension
c.URI = resolvedURI
c.Item = node.ToItem(r.DataFields)
c.Path = node.GetPath(r.PathDataFields)
// 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 {
logger.Log.Info("Content not found", zap.String("URI", r.URI))
c.Status = content.StatusNotFound
c.Dimension = r.Env.Dimensions[0]
logger.Log.Debug("Failed to resolve, falling back to default dimension",
zap.String("URI", r.URI),
zap.String("defaultDimension", 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
}
// 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 (repo *Repo) WriteRepoBytes(w io.Writer) {
f, err := os.Open(repo.history.getCurrentFilename())
if err != nil {
logger.Log.Error("Failed to serve Repo JSON", zap.Error(err))
}
_, _ = w.Write([]byte("{\"reply\":"))
_, err = io.Copy(w, f)
if err != nil {
logger.Log.Error("Failed to serve Repo JSON", zap.Error(err))
}
_, _ = w.Write([]byte("}"))
}
// 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))
}
logger.Log.Info("Update triggered")
// Log.Info(ansi.Yellow + "BUFFER LENGTH BEFORE tryUpdate(): " + strconv.Itoa(len(repo.jsonBuf.Bytes())) + ansi.Reset)
startTime := time.Now().UnixNano()
updateRepotime, updateErr := repo.tryUpdate()
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.Info(ansi.Yellow + "BUFFER LENGTH AFTER ERROR: " + strconv.Itoa(len(repo.jsonBuf.Bytes())) + ansi.Reset)
// only try to restore if the update failed during processing
if updateErr != errUpdateRejected {
updateResponse.ErrorMessage = updateErr.Error()
logger.Log.Error("Failed to update repository", zap.Error(updateErr))
restoreErr := repo.tryToRestoreCurrent()
if restoreErr != nil {
logger.Log.Error("Failed to restore preceding repository version", zap.Error(restoreErr))
} else {
logger.Log.Info("Successfully restored current repository from local history")
}
}
} else {
updateResponse.Success = true
// persist the currently loaded one
historyErr := repo.history.add(repo.jsonBuf.Bytes())
if historyErr != nil {
logger.Log.Error("Could not persist current repo in history", zap.Error(historyErr))
status.M.HistoryPersistFailedCounter.WithLabelValues().Inc()
}
// 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)
logger.Log.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 := repo.Directory[dimension]; ok {
logger.Log.Debug("Checking node",
zap.String("dimension", dimension),
zap.String("URI", testURI),
)
if repoNode, ok := d.URIDirectory[testURI]; ok {
resolved = true
logger.Log.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 true, testURI, dimension, repoNode
}
}
}
}
return
}
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 {
logger.Log.Error("maxGetURIForNodeRecursionLevel reached", zap.String("repoNode.ID", repoNode.ID), zap.String("linkID", repoNode.LinkID), zap.String("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,
exposeHiddenNodes bool,
) *content.Node {
node := content.NewNode()
node.Item = repoNode.ToItem(dataFields)
logger.Log.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] = repo.getNode(childNode, expanded, mimeTypes, path, level+1, groups, dataFields, exposeHiddenNodes)
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 errors.New(fmt.Sprint(
"unknown dimension ", envDimension,
" in r.Env must be one of ", availableDimensions,
" repo has ", len(repo.Directory), " dimensions",
))
}
}
return nil
}
func (repo *Repo) hasDimension(d string) bool {
_, hasDimension := repo.Directory[d]
return hasDimension
}

10
requests/content.go Normal file
View 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
View 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
View 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
View 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
View 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"`
}

4
requests/repo.go Normal file
View File

@ -0,0 +1,4 @@
package requests
// Repo - query repo
type Repo struct{}

View File

@ -1,63 +0,0 @@
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"`
}
// 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"`
}
// Nodes - which nodes in which dimensions
type Nodes struct {
// map[dimension]*node
Nodes map[string]*Node `json:"nodes"`
Env *Env `json:"env"`
}
// 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"`
}
// Update - request an update
type Update struct{}
// Repo - query repo
type Repo struct{}
// ItemMap - map of items
type ItemMap struct {
ID string `json:"id"`
DataFields []string `json:"dataFields"`
}
// URIs - request multiple URIs for a dimension use this resolve uris for links
// in a document
type URIs struct {
IDs []string `json:"ids"`
Dimension string `json:"dimension"`
}

4
requests/update.go Normal file
View File

@ -0,0 +1,4 @@
package requests
// Update - request an update
type Update struct{}

8
requests/uris.go Normal file
View File

@ -0,0 +1,8 @@
package requests
// URIs - request multiple URIs for a dimension use this resolve uris for links
// in a document
type URIs struct {
IDs []string `json:"ids"`
Dimension string `json:"dimension"`
}

View File

@ -1,6 +1,8 @@
package responses
import "fmt"
import (
"fmt"
)
// Error describes an error for humans and machines
type Error struct {
@ -21,19 +23,3 @@ func NewError(code int, message string) *Error {
Message: message,
}
}
// Update - information about an update
type Update struct {
// did it work or not
Success bool `json:"success"`
// this is for humand
ErrorMessage string `json:"errorMessage"`
Stats struct {
NumberOfNodes int `json:"numberOfNodes"`
NumberOfURIs int `json:"numberOfURIs"`
// seconds
RepoRuntime float64 `json:"repoRuntime"`
// seconds
OwnRuntime float64 `json:"ownRuntime"`
} `json:"stats"`
}

10
responses/stats.go Normal file
View File

@ -0,0 +1,10 @@
package responses
type Stats struct {
NumberOfNodes int `json:"numberOfNodes"`
NumberOfURIs int `json:"numberOfURIs"`
// seconds
RepoRuntime float64 `json:"repoRuntime"`
// seconds
OwnRuntime float64 `json:"ownRuntime"`
}

10
responses/update.go Normal file
View File

@ -0,0 +1,10 @@
package responses
// Update - information about an update
type Update struct {
// did it work or not
Success bool `json:"success"`
// this is for humans
ErrorMessage string `json:"errorMessage"`
Stats Stats `json:"stats"`
}

Some files were not shown because too many files have changed in this diff Show More