Compare commits

...

201 Commits
1.3.5 ... main

Author SHA1 Message Date
Daniel Thomas
e7e5d09f50
Merge pull request #52 from foomo/fix/get-repo-returns-stale-results
fix:  update before getRepo
2025-06-13 23:04:53 +02:00
Daniel Thomas
d5498dde73 fix: remove unuseful unit test 2025-06-13 23:02:19 +02:00
Daniel Thomas
fe1d9de6f1 fix: update persisted json after updating the repo using the poll routine 2025-06-13 22:21:59 +02:00
Miroslav Cvetic
74d2ff75f5 fix: add get repo test when update fails 2025-05-29 11:56:20 +02:00
Miroslav Cvetic
eefd5bbc57 fix: update before getRepo 2025-05-29 10:58:06 +02:00
Kevin Franklin Kim
29ca34e0d5
chore: add release.snapshot task 2025-05-28 17:29:42 +02:00
Kevin Franklin Kim
1fcc058fab
Merge pull request #50 from foomo/dependabot/github_actions/github-actions-afd9e15987
chore(deps): bump golangci/golangci-lint-action from 7 to 8 in the github-actions group
2025-05-19 11:23:09 +02:00
dependabot[bot]
b500a35214
chore(deps): bump golangci/golangci-lint-action
Bumps the github-actions group with 1 update: [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action).


Updates `golangci/golangci-lint-action` from 7 to 8
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 08:31:22 +00:00
Kevin Franklin Kim
c5d4b210f9
Merge pull request #49 from foomo/feature/build-with-safe-tag
feat: build with safe tag
2025-05-19 10:29:23 +02:00
Kevin Franklin Kim
d84f3f0941
feat: enable gzip 2025-05-19 09:57:22 +02:00
Kevin Franklin Kim
7a588bd108
fix: race 2025-05-19 09:42:01 +02:00
Kevin Franklin Kim
6c104da662
fix: race 2025-05-19 09:40:23 +02:00
Kevin Franklin Kim
d058ce2b41
fix: lint issues 2025-05-19 09:29:30 +02:00
Kevin Franklin Kim
1ac5dab2aa
fix: add mutex 2025-05-19 09:21:56 +02:00
Kevin Franklin Kim
a53de5827c
fix: log 2025-05-16 23:28:28 +02:00
Kevin Franklin Kim
b4b9de6df3
fix: t.Helper 2025-05-16 23:21:20 +02:00
Kevin Franklin Kim
368dcf4f3f
fix: close conn 2025-05-16 22:34:51 +02:00
Kevin Franklin Kim
ff7d3da5d1
revert: line break 2025-05-16 22:33:30 +02:00
Kevin Franklin Kim
c95742339f
feat: remove dead code 2025-05-16 22:32:28 +02:00
Kevin Franklin Kim
f0e84fc185
wip: debug 2025-05-16 17:12:31 +02:00
Kevin Franklin Kim
71155b45e6
fix: close server 2025-05-16 17:07:17 +02:00
Kevin Franklin Kim
2ea30be4b5
fix: pass through context 2025-05-16 16:59:58 +02:00
Kevin Franklin Kim
56aa5220ac
chore: disable gotestfmt 2025-05-16 16:52:09 +02:00
Kevin Franklin Kim
dd8e6a63e7
fix: lint issues 2025-05-16 16:45:36 +02:00
Kevin Franklin Kim
2bb303501e
feat: go 1.24.3 & deps 2025-05-16 16:43:23 +02:00
Kevin Franklin Kim
1e7676bf73
feat: bump alpine 2025-05-16 16:43:08 +02:00
Kevin Franklin Kim
680e0cdd33
feat: build with safe tag 2025-05-16 16:36:47 +02:00
Kevin Franklin Kim
924bc01c07
docs: update README 2025-05-16 16:30:57 +02:00
Kevin Franklin Kim
73203b24c0
chore: update gitignore 2025-05-16 16:30:46 +02:00
Kevin Franklin Kim
a2ae079ae9
chore: bump golangci-lint 2025-05-16 16:30:36 +02:00
Kevin Franklin Kim
327ca96a76
chore: add community files 2025-05-16 16:29:51 +02:00
Kevin Franklin Kim
5ea6ad27d2
Merge pull request #36 from foomo/fix/contentserver-gid-and-uid
fix: set gid & uid
2024-11-27 13:50:46 +01:00
Kevin Franklin Kim
ec843ef283
fix: set gid & uid 2024-11-27 13:48:41 +01:00
Kevin Franklin Kim
8fdeef555b
docs: update badge 2024-11-27 13:11:17 +01:00
Kevin Franklin Kim
15ab819b6a
Merge pull request #35 from foomo/fix/remove-readme-service
fix: remove readme service
2024-11-27 13:09:40 +01:00
Kevin Franklin Kim
c214ba4a4d
fix: remove readme service 2024-11-27 13:05:18 +01:00
Kevin Franklin Kim
0e61ff81ed
Merge pull request #34 from foomo/dependabot/docker/build/alpine-3.20.3
chore(deps): bump alpine from 3.19.1 to 3.20.3 in /build
2024-11-26 09:04:33 +01:00
Kevin Franklin Kim
8081e3c762
Merge pull request #33 from foomo/dependabot/github_actions/golangci/golangci-lint-action-6
chore(deps): bump golangci/golangci-lint-action from 4 to 6
2024-11-26 09:04:23 +01:00
Kevin Franklin Kim
1d0fa88b50
Merge pull request #32 from foomo/dependabot/github_actions/goreleaser/goreleaser-action-6
chore(deps): bump goreleaser/goreleaser-action from 5 to 6
2024-11-26 09:04:14 +01:00
dependabot[bot]
a57fafaf0f
chore(deps): bump alpine from 3.19.1 to 3.20.3 in /build
Bumps alpine from 3.19.1 to 3.20.3.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 21:44:12 +00:00
dependabot[bot]
620d759ce8
chore(deps): bump golangci/golangci-lint-action from 4 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 21:44:07 +00:00
dependabot[bot]
34d35ad779
chore(deps): bump goreleaser/goreleaser-action from 5 to 6
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 21:44:05 +00:00
Kevin Franklin Kim
318173f848
Merge pull request #31 from foomo/v1.11.x
Release v1.11.x
2024-11-25 22:43:27 +01:00
Kevin Franklin Kim
ec6ff54135
fix: lint issues 2024-11-25 22:40:02 +01:00
Kevin Franklin Kim
e50df5eaf9
test: close servers 2024-11-25 22:23:12 +01:00
Kevin Franklin Kim
ea920005e9
docs: update README 2024-11-25 09:43:28 +01:00
Kevin Franklin Kim
f8617b3da2
feat: bump go 2024-11-25 09:39:08 +01:00
Kevin Franklin Kim
71dfd39ff5
feat: add readiness healthzer 2024-03-27 13:43:59 +01:00
Kevin Franklin Kim
1617ebbd84
feat: handle closed connection 2024-03-25 09:19:01 +01:00
Kevin Franklin Kim
554733f1c7
feat: set keel version 2024-03-25 08:35:30 +01:00
Kevin Franklin Kim
9af5f53ce7
chore: update workflows 2024-03-25 08:26:39 +01:00
Kevin Franklin Kim
d46ba48b4b
feat: update keel 2024-03-22 21:24:48 +01:00
Kevin Franklin Kim
3717872187
fix: startup probe 2024-03-22 17:02:47 +01:00
Kevin Franklin Kim
1c472c13bf
fix: docker dir 2024-03-22 16:44:29 +01:00
Kevin Franklin Kim
b4f8b95a4b
refactor: rename onLoaded 2024-03-22 16:30:54 +01:00
Kevin Franklin Kim
ae481c5278
refactor: mark loaded 2024-03-22 16:26:34 +01:00
Kevin Franklin Kim
5e7798a53f
fix: base path flag 2024-03-22 16:09:33 +01:00
Kevin Franklin Kim
d9a8248c0a
feat: update init 2024-03-22 15:48:01 +01:00
Kevin Franklin Kim
abb6f692c7
fix: poll interval 2024-03-22 14:55:33 +01:00
Kevin Franklin Kim
52c1a67b7a
feat: add poll interval flag 2024-03-22 12:38:43 +01:00
Kevin Franklin Kim
3f8561359c
chore: fix typo 2024-03-22 11:47:29 +01:00
Kevin Franklin Kim
af95153899
chore: update gorelease 2024-03-22 11:41:12 +01:00
Kevin Franklin Kim
bb04560323
chore: fix secret 2024-03-22 10:53:03 +01:00
Kevin Franklin Kim
84135f3d8f
feat: update cmd flags 2024-03-22 10:38:47 +01:00
Kevin Franklin Kim
555fc8a5c2
chore: update release config 2024-03-22 09:06:09 +01:00
Kevin Franklin Kim
f1dae6a745
fix: race conditions 2024-03-21 18:01:24 +01:00
Kevin Franklin Kim
da832d3f52
chore: add husky and gh workflows 2024-03-21 16:10:08 +01:00
franklin
39cd0c422a
feat: upgrade 2024-03-21 16:03:21 +01:00
franklin
0c8267134e
feat: update deps 2024-03-19 20:15:09 +01:00
danielthomas74
3a4a69e2cf
Merge pull request #29 from foomo/feature/add-groups-to-content-node
feat: add groups to the content.node.item response
2022-06-09 13:59:36 +02:00
Daniel Thomas
cac32f2d52 feat: add groups to the content.node.item response 2022-06-09 11:54:46 +02:00
Jan Halfar
f3366f5211
Merge pull request #28 from foomo/update-polling
Update polling
2022-05-30 14:23:25 +02:00
Jan Halfar
2fa98dad5e
feat: polling support (#27) 2022-05-26 15:28:22 +02:00
Jan Halfar
be835e3ba6 Merge branch 'master' of github.com:foomo/contentserver into update-polling 2022-05-26 15:22:32 +02:00
Jan Halfar
efdfd11760 feat: polling support 2022-05-26 15:09:26 +02:00
Stefan Martinov
0e44ca809d
Deterministic Log Messaging & Refactoring (#26)
* feat: remove apex log from dependency list & upgrade deps

* chore: remove commented out code

* chore: remove high cardinality metric labels

* chore: add stack trace to failed repo requests

* chore: remove logging and return error instead

* chore: change log messages for contentserver

* chore: add constistent messages to content update routine

* feat: add runtime id, and multierr validator

* chore: rename history

* chore: golint fixes

* chore: simplify (golinter)

* chore: update go version

* chore: remove unused dependencies for new go

* chore: move content update to else statement

* chore: remove go-spew

* chore: bump go version to 1.18

* chore: remove alpine dep for updated deps

* chore: update go version and improve build

* chore: minor reformatting
2022-05-26 15:09:11 +02:00
Philipp Mieden
3440cbdc0e fix: improve url validation 2021-10-07 12:00:14 +02:00
Philipp Mieden
c2837eec07 fix: validate server url in NewHTTPClient and return an error for invalid configurations 2021-10-07 10:40:02 +02:00
Stefan Martinov
006bd6ea0a feat: update repository path for docker hub 2021-09-22 11:59:51 +02:00
Stefan Martinov
ac85c31b77
Configurable Repository Timeout (#25)
* feat: implement correct prometheus metrics

* feat: remove nodeID from metrics due to high cardinality

* feat: add configurable timeouts to contentserver
2021-09-22 11:52:53 +02:00
franklin
93fcca1a5f feat: make http transport client exchangeable 2021-07-30 17:37:10 +02:00
Jan Halfar
ae29f1060c
Merge pull request #24 from foomo/feature/get-content-path-data-fields
feat: add PathDataFields option on GetContent
2021-07-01 09:53:46 +02:00
franklin
5788fd203d feat: add PathDataFields option on GetContent 2021-06-30 15:53:16 +02:00
Stefan Martinov
c9bf5666e5
Merge pull request #23 from foomo/feature/metrics-and-logging
feat: update metrics & logging
2020-11-23 12:38:02 +01:00
Stefan Martinov
ffa04ace36 feat: add better metrics for errors 2020-11-23 11:33:36 +01:00
Stefan Martinov
24c65ba8df chore: return logging of error requested 2020-11-23 09:29:47 +01:00
Stefan Martinov
ddb4adf571 feat: add update request queue test 2020-11-22 18:53:16 +01:00
Frederik Löffert
0ab7935a05
response wrapper to fix json unmarshal issues (#21)
* chore: change wrapping

* fix: fix mapping bugs

Co-authored-by: Stefan Martinov <stefan.martinov@gmail.com>
2020-04-01 12:25:55 +02:00
Frederik Löffert
f75f3a09ae Dockerfile build cleanup 2020-03-30 23:16:48 +02:00
Frederik Löffert
422fcf95b6 go version updated to 1.14 2020-03-30 22:52:01 +02:00
Frederik Löffert
7280bc5e03 Dockerfile: updated go to version 1.14 and alpine to 3.11 2020-03-30 22:46:40 +02:00
Cristian Vidmar
2b9d7ea9e7 ExposeHiddenNodes in request 2020-03-16 18:02:25 +01:00
Stefan Martinov
934b1dc9c4 fix: add update log queue rejection 2019-10-25 16:37:05 +02:00
Stefan Martinov
620291db4a feat: remove errors in update queue and add recovery mechanism 2019-09-25 17:27:00 +02:00
Stefan Martinov
130ab553e3
Merge pull request #19 from foomo/feature/contentserver-panic-fix
ContentServer Panic Fix
2019-09-07 09:44:35 +02:00
Stefan Martinov
5ce4cccbea feat: add panic handlers 2019-09-06 17:26:02 +02:00
Stefan Martinov
f7f3b8096e chore: join if clause 2019-09-06 17:15:04 +02:00
Stefan Martinov
3f02444b97 fix: remove offending log nil pointers 2019-09-06 17:13:46 +02:00
Stefan Martinov
7e59a0dc71 chore; update go modules to 1.13 2019-09-06 17:08:58 +02:00
Phil
864df48b90
travis badge: use master branch 2019-06-04 15:18:31 +02:00
Philipp Mieden
b41951f51e run tests in verbose mode 2019-06-04 15:13:33 +02:00
Phil
16fd473458
Merge pull request #15 from foomo/feature/memdebug
increased delays for travis CI tests
2019-06-04 12:26:12 +02:00
Philipp Mieden
6bf4e20444 increased delays for travis CI tests 2019-06-04 12:21:42 +02:00
Phil
fd0c81bc23
Merge pull request #14 from foomo/feature/memdebug
merge feature/memdebug
2019-06-04 08:57:20 +02:00
Philipp Mieden
25afa0523d added delays to tests for travis 2019-06-04 08:55:07 +02:00
Philipp Mieden
aed699f987 Merge branch 'feature/memdebug' of github.com:foomo/contentserver into feature/memdebug 2019-06-03 19:27:12 +02:00
Philipp Mieden
1911f68de6 set go modules var in make dep 2019-06-03 19:27:06 +02:00
Frederik Löffert
d15c524be8
use makefile in travis CI 2019-06-03 19:24:06 +02:00
Frederik Löffert
0919233472
enabled gomods for travis CI 2019-06-03 19:19:32 +02:00
Philipp Mieden
473da013c9 Merge branch 'feature/memdebug' of github.com:foomo/contentserver into feature/memdebug 2019-06-03 19:10:58 +02:00
Philipp Mieden
2ab4c0364c updated go modules 2019-06-03 19:10:42 +02:00
Frederik Löffert
fd4f87da95
update go version (1.12) in travis CI 2019-06-03 19:06:20 +02:00
Philipp Mieden
0661a69601 removed version flag 2019-06-03 12:16:48 +02:00
Philipp Mieden
e6e95db586 fixed log rotation and updated tests accordingly 2019-05-29 15:23:42 +02:00
Philipp Mieden
a2b0eabb41 logging: set console encoding explicitely if LOG_JSON env var is not set 2019-05-29 15:17:47 +02:00
Philipp Mieden
f5d1117c67 set explicit go version in Dockerfile, current latest: 1.12.5 2019-05-29 14:19:07 +02:00
Philipp Mieden
6381c7c0c2 added metric for failed attempts to persist the content history 2019-05-29 14:14:15 +02:00
Philipp Mieden
581e68599c added flag to set the maximum number of history versions, set default to 1 2019-05-29 13:59:18 +02:00
Philipp Mieden
1c814a450c testclient: log number of dimensions for GetRepo 2019-05-29 13:58:14 +02:00
Philipp Mieden
2cf28f7217 updated graphics for horizontal scaling 2019-05-29 12:23:55 +02:00
Philipp Mieden
8197ec0931 removed version flag: git tags are used for versioning 2019-05-27 14:35:06 +02:00
Philipp Mieden
8d85fc5f81 GetRepo cleanup and testing, fixed unit tests 2019-05-27 12:11:16 +02:00
Philipp Mieden
0aed28b524 removed links 2019-05-27 10:45:32 +02:00
Philipp Mieden
fcc36028c1 Merge branch 'feature/memdebug' of github.com:foomo/contentserver into feature/memdebug 2019-05-27 10:23:37 +02:00
Philipp Mieden
1d3405cbf7 updated graphics and readme 2019-05-27 10:23:28 +02:00
Philipp Mieden
9e8a0cb6d3 cleanup 2019-05-24 17:45:44 +02:00
Philipp Mieden
735a0ab3f8 cleanup 2019-05-24 17:45:44 +02:00
Philipp Mieden
3b4a55f18e added flags to testclient, added optional getrepo call 2019-05-24 17:40:26 +02:00
Philipp Mieden
a5ff003d8f added flags to testclient, added optional getrepo call 2019-05-24 17:40:26 +02:00
Philipp Mieden
871c844f7b refactored GetRepo: serve JSON from fs and write directly into http.ResponseWriter 2019-05-24 16:41:57 +02:00
Philipp Mieden
8224e92d4d Merge branch 'feature/memdebug' of github.com:foomo/contentserver into feature/memdebug 2019-05-24 16:23:28 +02:00
Philipp Mieden
1b7aa6475e skip nodeRequests with empty name or id 2019-05-24 16:23:20 +02:00
Frederik Löffert
aa0f6695d7
disabled debug mode on default 2019-05-24 15:14:44 +02:00
Frederik Löffert
4e6eecc673
loglevel removed from Dockerfile 2019-05-24 15:02:15 +02:00
Philipp Mieden
69dec41605 refactored repo to reuse a bytes.Buffer for updates, to reduce the number of allocations 2019-05-24 13:00:28 +02:00
Philipp Mieden
5cff674940 working on prometheus dashboard 2019-05-23 17:51:20 +02:00
Philipp Mieden
33364e3af8 added metric constructors 2019-05-23 16:33:01 +02:00
Philipp Mieden
647853292b added new metrics 2019-05-23 16:23:24 +02:00
Philipp Mieden
79d828bb23 implemented stefans feedback on prometheus metrics 2019-05-23 15:29:07 +02:00
Philipp Mieden
7c29ec73e4 updated Dockerfile to log json in production 2019-05-23 14:23:42 +02:00
Philipp Mieden
71403194e2 removed log level flag 2019-05-23 14:23:26 +02:00
Philipp Mieden
e9245a200c replaced logger, fixed update queue mechanism 2019-05-23 14:20:38 +02:00
Philipp Mieden
d9f6cc60c4 cleanup 2019-05-23 10:39:44 +02:00
Philipp Mieden
e874cc2b16 implemented queuing update requests and canceling new ones when the queue is full 2019-05-23 10:35:30 +02:00
Philipp Mieden
2decb53ec1 added main datastructures to graffle 2019-05-23 09:17:09 +02:00
Philipp Mieden
2faa088178 added more profiling targets to Makefile 2019-05-23 09:16:47 +02:00
Philipp Mieden
e2a51bb5a5 updated testclient logging 2019-05-23 09:16:04 +02:00
Frederik Löffert
0056f53b97 Merge branch 'feature/memdebug' of github.com:foomo/contentserver into feature/memdebug 2019-05-22 18:11:34 +02:00
Philipp Mieden
28292faea9 commented out unused RepoNode constructor, updated pprof 2019-05-22 09:52:58 +02:00
Philipp Mieden
a4097c05f4 added targets for testing 2019-05-21 17:30:39 +02:00
Philipp Mieden
97633dc9d9 debug mode: only print number of json bytes and dont dump the entire beast to stdout 2019-05-21 17:30:20 +02:00
Philipp Mieden
eca5e3b4f0 fixed loops for mem profiling 2019-05-21 17:29:16 +02:00
Frederik Löffert
f7aea048d3 lock update requests 2019-05-21 16:06:49 +02:00
Philipp Mieden
1449d6902c server cleanup 2019-05-21 15:01:13 +02:00
Philipp Mieden
18897d2e32 updated deps 2019-05-21 12:36:06 +02:00
Philipp Mieden
0827eb9b4a added makefile section comments 2019-05-21 12:35:56 +02:00
Philipp Mieden
63640b24b2 updated makefile targets for profiling 2019-05-21 12:11:54 +02:00
Philipp Mieden
3985784579 added pprof debug server 2019-05-21 12:11:39 +02:00
Philipp Mieden
55fd63b82d added memory profiling flag for dumping heap periodically 2019-05-21 12:11:16 +02:00
Philipp Mieden
295cdf66fc flag naming convention 2019-05-21 11:45:47 +02:00
Philipp Mieden
03d5b36706 repo history code cleanup 2019-05-21 11:43:28 +02:00
Philipp Mieden
65b6b2341a added free-os-mem flag to contentserver 2019-05-21 11:43:06 +02:00
Philipp Mieden
0735b5ad18 code cleanup, grouping declarations 2019-05-21 11:17:03 +02:00
Philipp Mieden
f20402ef5f git ignore profiling files 2019-05-21 11:10:33 +02:00
Philipp Mieden
dc047baf32 repo code cleanup 2019-05-21 11:10:05 +02:00
Philipp Mieden
f0df9a6322 added profile-test target to makefile 2019-05-21 11:09:51 +02:00
Philipp Mieden
5e44495adc replaced encoding/json with jsoniter high performance pkg 2019-05-21 10:59:54 +02:00
Philipp Mieden
e64b07f6c6 fixed repo test assertion messages, added benchmark for loading test repo 2019-05-21 10:51:21 +02:00
Philipp Mieden
b713a41cf6 updated gitignore to ignore local var dir 2019-05-21 10:50:47 +02:00
Philipp Mieden
5ee042bcd4 handle unchecked errors 2019-05-21 10:12:05 +02:00
Philipp Mieden
54ee995295 commented out unused symbols 2019-05-21 10:07:13 +02:00
Philipp Mieden
82733d4b25 fix: call of Unmarshal passed a non-pointer as second argument 2019-05-21 10:06:21 +02:00
Philipp Mieden
dd902fe717 removed redundant return statement 2019-05-21 10:05:24 +02:00
Philipp Mieden
d2e0af8aca using fmt.Errorf(...) instead of errors.New(fmt.Sprintf(...)) 2019-05-21 10:05:01 +02:00
Philipp Mieden
a267dbe1ec history_test cleanup and formatting 2019-05-21 10:04:09 +02:00
Philipp Mieden
bdb694b0ff simplified byte equality comparison 2019-05-21 10:03:31 +02:00
Philipp Mieden
9d94c09735 grouped variable declarations 2019-05-21 09:56:38 +02:00
Philipp Mieden
7f9b32162e used a simple channel send/receive instead of with a single case 2019-05-21 09:55:55 +02:00
Philipp Mieden
284ee99690 omitted comparison to bool constant 2019-05-21 09:55:36 +02:00
Philipp Mieden
73b9b71dd3 removed unused error assignments, code cleanup 2019-05-21 09:52:59 +02:00
Philipp Mieden
8e574d0675 merged metrics branch 2019-05-21 09:49:48 +02:00
Philipp Mieden
757c310f8d handlerequest cleanup 2019-05-21 09:36:40 +02:00
Philipp Mieden
42adb6a25a simplified loop over channel inputs 2019-05-21 09:19:25 +02:00
Frederik Löffert
5beb586365 Merge branch 'develop' of github.com:foomo/contentserver into feature/metrics 2019-05-14 16:43:47 +02:00
Frederik Löffert
79244f8ab3 add "dep" command to makefile
hide vendor in gitignore
2019-05-14 16:43:19 +02:00
Frederik Löffert
b7f10ed673 add prometheus request metrics 2019-05-14 16:40:25 +02:00
Stefan Martinov
ff28d6670f feat: add healthz handler to contentserver 2019-05-08 18:21:20 +02:00
Stefan Martinov
d0e9a2966a chore: add correct expose file and better makefile 2019-05-08 18:04:58 +02:00
Stefan Martinov
6b891e7f0f chore: update modules & testing 2019-05-08 17:57:08 +02:00
Stefan Martinov
3eac4bd1b7 feat: add prometheus handler and go-modules 2019-05-08 17:51:51 +02:00
Jan Halfar
8f7c23ff4a added DataFields to content request 2018-11-28 11:56:11 +01:00
Jan Halfar
3479cd7eaf 1.4.1 no more default socket server 2018-11-27 18:01:27 +01:00
Jan Halfar
a120c09c82 skipping historic go versions 2018-11-27 13:45:23 +01:00
Jan Halfar
f96a5669a8 made webserver path configurable 2018-11-27 13:28:20 +01:00
Jan Halfar
37147120e2 safer access to repo.Directory in _updateDimension 2018-11-27 12:56:38 +01:00
Jan Halfar
2c5492ebe4 added webserver 2018-11-27 12:53:12 +01:00
Jan Halfar
bbded7c9db better error with empty dimension, in validateContentRequest 2018-11-27 12:52:16 +01:00
Jan Halfar
6eb33c6978 added level in logging 2018-11-27 12:51:19 +01:00
Jan Halfar
ef3ef2bc50 new socket client with connection pool and added http client 2018-11-27 12:50:10 +01:00
Jan Halfar
1a64987d8d added tag 2018-11-27 12:49:10 +01:00
Jan Halfar
7decb6b56f version bump 2018-11-27 12:49:02 +01:00
108 changed files with 6418 additions and 1883 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

21
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,21 @@
# Contributing
If you want to submit a pull request to fix a bug or enhance an existing
feature, please first open an issue and link to that issue when you
submit your pull request.
If you have any questions about a possible submission, feel free to open
an issue too.
### Pull request process
1. Fork this repository
2. Create a branch in your fork to implement the changes. We recommend using
the issue number as part of your branch name, e.g. `1234-fixes`
3. Ensure that any documentation is updated with the changes that are required
by your fix.
4. Ensure that any samples are updated if the base image has been changed.
5. Submit the pull request. *Do not leave the pull request blank*. Explain exactly
what your changes are meant to do and provide simple steps on how to validate
your changes. Ensure that you reference the issue you created as well.
The pull request will be review before it is merged.

17
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,17 @@
---
name: Bug Report
about: Report a bug you encountered
labels: bug
---
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:
**Environment**:
- Affected Version:
- OS (e.g: `cat /etc/os-release`):
- Others:

8
.github/ISSUE_TEMPLATE/enhancement.md vendored Normal file
View File

@ -0,0 +1,8 @@
---
name: Enhancement Request
about: Suggest an enhancement
labels: enhancement
---
**What would you like to be added**:
**Why is this needed**:

19
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
### Type of Change
- [ ] New feature
- [ ] Bug fix
- [ ] Documentation update
- [ ] Refactoring
- [ ] Hotfix
- [ ] Security patch
### Description
_[Provide a detailed explanation of the changes you have made. Include the reasons behind these changes and any relevant context. Link any related issues.]_
### Related Issues
_[If this pull request addresses an issue, please link to it here (e.g., Fixes #123).]_
### Checklist
- [ ] My code adheres to the coding and style guidelines of the project.
- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, particularly in hard-to-understand areas.
- [ ] I have made corresponding changes to the documentation.

45
.github/SECURITY.md vendored Normal file
View File

@ -0,0 +1,45 @@
# Security Guidelines
## How security is managed on this project
The foomo team and community take security seriously and wants to ensure that
we maintain a secure environment and provide secure solutions for the open
source community. To help us achieve these goals, please note the
following before using this software:
- Review the software license to understand the contributor's obligations in
terms of warranties and suitability for purpose
- For any questions or concerns about security, you can
[create an issue][new-issue] or [report a vulnerability][new-sec-issue]
- We request that you work with our security team and opt for
responsible disclosure using the guidelines below
- All security related issues and pull requests you make should be tagged with
"security" for easy identification
- Please monitor this repository and update your environment in a timely manner
as we release patches and updates
## Responsibly Disclosing Security Bugs
If you find a security bug in this repository, please work with contributors
following responsible disclosure principles and these guidelines:
- Do not submit a normal issue or pull request in our public repository, instead
[report it directly][new-sec-issue].
- We will review your submission and may follow up for additional details
- If you have a patch, we will review it and approve it privately; once approved
for release you can submit it as a pull request publicly in the repository (we
give credit where credit is due)
- We will keep you informed during our investigation, feel free to check in for
a status update
- We will release the fix and publicly disclose the issue as soon as possible,
but want to ensure we due properly due diligence before releasing
- Please do not publicly blog or post about the security issue until after we
have updated the public repo so that other downstream users have an opportunity
to patch
## Contact / Misc
If you have any questions, please reach out directly by [creating an issue][new-issue].
[new-issue]: https://github.com/foomo/contentserver/issues/new/choose
[new-sec-issue]: https://github.com/foomo/contentserver/security/advisories/new

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

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

@ -0,0 +1,52 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
open-pull-requests-limit: 1
directory: '/'
schedule:
day: 'sunday'
interval: 'weekly'
groups:
github-actions:
patterns: ['*']
- package-ecosystem: 'gomod'
open-pull-requests-limit: 1
directory: '/'
schedule:
day: 'sunday'
interval: 'weekly'
groups:
gomod-security:
applies-to: security-updates
update-types: ['minor', 'patch']
patterns: ['*']
gomod-update:
applies-to: version-updates
update-types: ['minor', 'patch']
patterns: ['*']
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]
- package-ecosystem: docker
open-pull-requests-limit: 1
directory: '/build'
schedule:
day: 'sunday'
interval: 'weekly'
groups:
docker-security:
applies-to: security-updates
update-types: ['minor', 'patch']
patterns: ['*']
docker-update:
applies-to: version-updates
update-types: ['minor', 'patch']
patterns: ['*']
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]

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

@ -0,0 +1,37 @@
name: checks
on:
push:
branches: [ main ]
pull_request:
merge_group:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- uses: gotesttools/gotestfmt-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: golangci/golangci-lint-action@v8
with:
version: latest
- name: Run tests
run: make test
- uses: coverallsapp/github-action@v2
with:
file: coverage.out

43
.github/workflows/tag.yml vendored Normal file
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@v6
with:
version: latest
args: release --clean --timeout=90m
env:
GITHUB_TOKEN: ${{ steps.app_token.outputs.token }}

30
.gitignore vendored
View File

@ -1,5 +1,29 @@
.*
*~
*.zip
*.tar
*.out
*.log
/bin/
/pkg/tmp/
!.git*
/dist/
/tmp/
## Git
!.gitkeep
!.gitignore
## GitHub
!.github/
## Editorconfig
!.editorconfig
## Husky
!.husky/
!.husky.yaml
## Ownbrew
!.ownbrew.yaml
## Golang
!.golangci.yml
!.goreleaser.yml

152
.golangci.yml Normal file
View File

@ -0,0 +1,152 @@
version: "2"
run:
go: 1.24.3
build-tags: [safe]
modules-download-mode: readonly
linters:
default: none
enable:
## Default linters
- errcheck # errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
- govet # (vet, vetshadow) Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes. [fast: false, auto-fix: false]
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
- staticcheck # (megacheck) It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
- unused # (megacheck) Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
## Recommended linters
- asasalint # check for pass []any as any in variadic func(...any) [fast: false, auto-fix: false]
- asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name [fast: true, auto-fix: false]
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
- bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
- canonicalheader # checks whether net/http.Header uses canonical header [fast: false, auto-fix: false]
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: false, auto-fix: false]
- contextcheck # check whether the function uses a non-inherited context [fast: false, auto-fix: false]
- copyloopvar # (go >= 1.22) copyloopvar is a linter detects places where loop variables are copied [fast: true, auto-fix: false]
- decorder # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false]
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false]
#- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false]
#- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix]
- fatcontext # detects nested contexts in loops and function literals [fast: false, auto-fix: false]
#- forbidigo # Forbids identifiers [fast: false, auto-fix: false]
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false]
- gochecksumtype # Run exhaustiveness checks on Go "sum types" [fast: false, auto-fix: false]
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
- gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: true]
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false]
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false]
- goprintffuncname # Checks that printf-like functions are named with `f` at the end. [fast: true, auto-fix: false]
- gosec # (gas) Inspects source code for security problems [fast: false, auto-fix: false]
- gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase [fast: false, auto-fix: false]
- grouper # Analyze expression groups. [fast: true, auto-fix: false]
- iface # Detect the incorrect use of interfaces, helping developers avoid interface pollution. [fast: false, auto-fix: false]
- importas # Enforces consistent import aliases [fast: false, auto-fix: false]
- inamedparam # reports interfaces with unnamed method parameters [fast: true, auto-fix: false]
#- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false]
- loggercheck # (logrlint) Checks key value pairs for common logger libraries (kitlog,klog,logr,zap). [fast: false, auto-fix: false]
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
- mirror # reports wrong mirror patterns of bytes/strings usage [fast: false, auto-fix: true]
- misspell # Finds commonly misspelled English words [fast: true, auto-fix: true]
- musttag # enforce field tags in (un)marshaled structs [fast: false, auto-fix: false]
- nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). [fast: true, auto-fix: false]
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
- nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error.
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
- noctx # Finds sending http request without context.Context [fast: false, auto-fix: false]
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: true]
#- nonamedreturns # Reports all named returns [fast: false, auto-fix: false]
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. [fast: true, auto-fix: false]
#- paralleltest # Detects missing usage of t.Parallel() method in your Go test [fast: false, auto-fix: false]
- predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false]
- promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false]
- reassign # Checks that package variables are not reassigned [fast: false, auto-fix: false]
- recvcheck # checks for receiver type consistency [fast: false, auto-fix: false]
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
- rowserrcheck # checks whether Rows.Err of rows is checked successfully [fast: false, auto-fix: false]
- spancheck # Checks for mistakes with OpenTelemetry/Census spans. [fast: false, auto-fix: false]
- sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed. [fast: false, auto-fix: false]
- testableexamples # linter checks if examples are testable (have an expected output) [fast: true, auto-fix: false]
- testifylint # Checks usage of github.com/stretchr/testify. [fast: false, auto-fix: false]
#- testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
- thelper # thelper detects tests helpers which is not start with t.Helper() method. [fast: false, auto-fix: false]
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. [fast: false, auto-fix: false]
- unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false]
- usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false]
- usetesting # Reports uses of functions with replacement inside the testing package. [auto-fix]
- wastedassign # Finds wasted assignment statements [fast: false, auto-fix: false]
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. [fast: true, auto-fix: true]
## Discouraged linters
#- cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false]
#- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
#- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
#- dupl # Tool for code clone detection [fast: true, auto-fix: false]
#- dupword # checks for duplicate words in the source code [fast: true, auto-fix: false]
#- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
#- err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false]
#- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false]
#- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
#- ginkgolinter # enforces standards of using ginkgo and gomega [fast: false, auto-fix: false]
#- gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false]
#- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
#- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
#- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
#- godot # Check if comments end in a period [fast: true, auto-fix: true]
#- godox # Tool for detection of comment keywords [fast: true, auto-fix: false]
#- interfacebloat # A linter that checks the number of methods inside an interface. [fast: true, auto-fix: false]
#- intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false]
#- ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false]
#- lll # Reports long lines [fast: true, auto-fix: false]
#- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false]
#- nestif # Reports deeply nested if statements [fast: true, auto-fix: false]
#- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false]
#- mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
#- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. [fast: false, auto-fix: false]
#- prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false]
#- protogetter # Reports direct reads from proto message fields when getters should be used [fast: false, auto-fix: true]
#- sloglint # ensure consistent code style when using log/slog [fast: false, auto-fix: false]
#- tagalign # check that struct tags are well aligned [fast: true, auto-fix: true]
#- tagliatelle # Checks the struct tags. [fast: true, auto-fix: false]
#- unparam # Reports unused function parameters [fast: false, auto-fix: false]
#- varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false]
#- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
#- wsl # add or remove empty lines [fast: true, auto-fix: false]
#- zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg` [fast: false, auto-fix: false]
settings:
gocritic:
disabled-checks:
- ifElseChain
- commentFormatting
revive:
rules:
- name: unused-parameter
disabled: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- bin
- tmp
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- bin
- tmp
- third_party$
- builtin$
- examples$

119
.goreleaser.yml Normal file
View File

@ -0,0 +1,119 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: contentserver
release:
github:
owner: foomo
name: contentserver
prerelease: auto
builds:
- binary: contentserver
main: ./main.go
env:
- CGO_ENABLED=0
goos:
- windows
- darwin
- linux
goarch:
- amd64
- arm64
goarm:
- '7'
flags:
- -trimpath
- -tags=safe
ldflags:
- -s -w -X github.com/foomo/contentserver/cmd.version={{.Version}}
archives:
- formats: [ tar.gz ]
format_overrides:
- goos: windows
formats: [ zip ]
changelog:
use: github-native
brews:
- repository:
owner: foomo
name: homebrew-tap
caveats: "contentserver --help"
homepage: "https://github.com/foomo/contentserver"
description: "Serves content tree structures very quickly"
test: |
system "#{bin}/contentserver version"
dockers:
- use: buildx
goos: linux
goarch: amd64
dockerfile: build/buildx.Dockerfile
image_templates:
- '{{ if eq .Prerelease "" }}foomo/contentserver:latest-amd64{{ end }}'
- 'foomo/contentserver:{{ .Version }}-amd64'
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}-amd64{{ end }}'
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}.{{ .Minor }}-amd64{{ end }}'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Serves content tree structures very quickly'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation={{.GitURL}}'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/amd64'
- use: buildx
goos: linux
goarch: arm64
dockerfile: build/buildx.Dockerfile
image_templates:
- '{{ if eq .Prerelease "" }}foomo/contentserver:latest-arm64{{ end }}'
- 'foomo/contentserver:{{ .Version }}-arm64'
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}-arm64{{ end }}'
- '{{ if eq .Prerelease "" }}foomo/contentserver:{{ .Major }}.{{ .Minor }}-arm64{{ end }}'
build_flag_templates:
- '--pull'
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
- '--label=org.opencontainers.image.title={{.ProjectName}}'
- '--label=org.opencontainers.image.description=Serves content tree structures very quickly'
- '--label=org.opencontainers.image.source={{.GitURL}}'
- '--label=org.opencontainers.image.url={{.GitURL}}'
- '--label=org.opencontainers.image.documentation={{.GitURL}}'
- '--label=org.opencontainers.image.created={{.Date}}'
- '--label=org.opencontainers.image.revision={{.FullCommit}}'
- '--label=org.opencontainers.image.version={{.Version}}'
- '--platform=linux/arm64'
docker_manifests:
# basic
- name_template: 'foomo/contentserver:latest'
image_templates:
- 'foomo/contentserver:latest-amd64'
- 'foomo/contentserver:latest-arm64'
skip_push: auto
- name_template: 'foomo/contentserver:{{ .Version }}'
image_templates:
- 'foomo/contentserver:{{ .Version }}-amd64'
- 'foomo/contentserver:{{ .Version }}-arm64'
- name_template: 'foomo/contentserver:{{ .Major }}'
image_templates:
- 'foomo/contentserver:{{ .Major }}-amd64'
- 'foomo/contentserver:{{ .Major }}-arm64'
skip_push: auto
- name_template: 'foomo/contentserver:{{ .Major }}.{{ .Minor }}'
image_templates:
- 'foomo/contentserver:{{ .Major }}.{{ .Minor }}-amd64'
- 'foomo/contentserver:{{ .Major }}.{{ .Minor }}-arm64'
skip_push: auto

15
.husky.yaml Normal file
View File

@ -0,0 +1,15 @@
hooks:
pre-commit:
- golangci-lint run --fast-only
- husky lint-staged
commit-msg:
# only execute if not in a merge
- if [[ -z $(git rev-parse -q --verify MERGE_HEAD) ]]; then husky lint-commit; fi
lint-staged:
'*.go':
- golangci-lint fmt
lint-commit:
types: '^(feat|fix|build|chore|docs|perf|refactor|revert|style|test|wip)$'
header: '^(?P<type>\w+)(\((?P<scope>[\w/.-]+)\))?(?P<breaking>!)?:( +)?(?P<header>.+)'

3
.husky/applypatch-msg Executable file
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,6 +0,0 @@
language: go
go:
- 1.5
- 1.6
- 1.7
- tip

View File

@ -1,21 +0,0 @@
FROM scratch
COPY bin/contentserver-linux-amd64 /usr/sbin/contentserver
# install ca root certificates
# https://curl.haxx.se/docs/caextract.html
# http://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
# does not work on docker for mac :(
# ADD https://curl.haxx.se/ca/cacert.pem /etc/ssl/certs/ca-certificates.crt
ADD .cacert.pem /etc/ssl/certs/ca-certificates.crt
ENV CONTENT_SERVER_LOG_LEVEL=error
ENV CONTENT_SERVER_ADDR=0.0.0.0:80
ENV CONTENT_SERVER_VAR_DIR=/var/lib/contentserver
VOLUME $CONTENT_SERVER_VAR_DIR
EXPOSE 80
ENTRYPOINT ["/usr/sbin/contentserver"]
CMD ["-address=$CONTENT_SERVER_ADDR", "-log-level=$CONTENT_SERVER_LOG_LEVEL", "-var-dir=$CONTENT_SERVER_VAR_DIR"]

124
Makefile
View File

@ -1,23 +1,109 @@
SHELL := /bin/bash
.DEFAULT_GOAL:=help
-include .makerc
TAG=`git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || git rev-parse --abbrev-ref HEAD`
# --- Targets -----------------------------------------------------------------
all: build test
clean:
rm -fv bin/contentserve*
build: clean
go build -o bin/contentserver
build-arch: clean
GOOS=linux GOARCH=amd64 go build -o bin/contentserver-linux-amd64
GOOS=darwin GOARCH=amd64 go build -o bin/contentserver-darwin-amd64
build-docker: clean build-arch
curl https://curl.haxx.se/ca/cacert.pem > .cacert.pem
docker build -q . > .image_id
docker tag `cat .image_id` docker-registry.bestbytes.net/contentserver:$(TAG)
echo "# tagged container `cat .image_id` as docker-registry.bestbytes.net/contentserver:$(TAG)"
rm -vf .image_id .cacert.pem
# This allows us to accept extra arguments
%: .husky
@:
package: build
pkg/build.sh
.PHONY: .husky
# Configure git hooks for husky
.husky:
@if ! command -v husky &> /dev/null; then \
echo "ERROR: missing executeable 'husky', please run:"; \
echo "\n$ go install github.com/go-courier/husky/cmd/husky@latest\n"; \
fi
@git config core.hooksPath .husky
## === Tasks ===
.PHONY: doc
## Open go docs
doc:
@open "http://localhost:6060/pkg/github.com/foomo/contentserver/"
@godoc -http=localhost:6060 -play
.PHONY: test
## Run tests
test:
go test ./...
@GO_TEST_TAGS=-skip go test -v -tags=safe -coverprofile=coverage.out -race -count=1 ./...
#@GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -race -json ./... | gotestfmt
.PHONY: test.update
## Run tests and update snapshots
test.update:
@GO_TEST_TAGS=-skip go test -update -v -tags=safe -coverprofile=coverage.out -race ./...
#@GO_TEST_TAGS=-skip go test -update -tags=safe -coverprofile=coverage.out -race -json ./... | gotestfmt
.PHONY: lint
## Run linter
lint:
@golangci-lint run
.PHONY: lint.fix
## Fix lint violations
lint.fix:
@golangci-lint run --fix
.PHONY: tidy
## Run go mod tidy
tidy:
@go mod tidy
.PHONY: outdated
## Show outdated direct dependencies
outdated:
@go list -u -m -json all | go-mod-outdated -update -direct
.PHONY: install
## Install binary
install:
@go build -tags=safe -o ${GOPATH}/bin/contentserver main.go
.PHONY: build
## Build binary
build:
@mkdir -p bin
@go build -tags=safe -o bin/contentserver main.go
.PHONY: release.snapshot
## Create a goreleaser snapshot release
release.snapshot:
@rm -rf ./dist
@goreleaser release --snapshot
## === Utils ===
.PHONY: help
## Show help text
help:
@awk '{ \
if ($$0 ~ /^.PHONY: [a-zA-Z\-\_0-9]+$$/) { \
helpCommand = substr($$0, index($$0, ":") + 2); \
if (helpMessage) { \
printf "\033[36m%-23s\033[0m %s\n", \
helpCommand, helpMessage; \
helpMessage = ""; \
} \
} else if ($$0 ~ /^[a-zA-Z\-\_0-9.]+:/) { \
helpCommand = substr($$0, 0, index($$0, ":")); \
if (helpMessage) { \
printf "\033[36m%-23s\033[0m %s\n", \
helpCommand, helpMessage"\n"; \
helpMessage = ""; \
} \
} else if ($$0 ~ /^##/) { \
if (helpMessage) { \
helpMessage = helpMessage"\n "substr($$0, 3); \
} else { \
helpMessage = substr($$0, 3); \
} \
} else { \
if (helpMessage) { \
print "\n "helpMessage"\n" \
} \
helpMessage = ""; \
} \
}' \
$(MAKEFILE_LIST)

102
README.md
View File

@ -1,59 +1,95 @@
[![Travis CI](https://travis-ci.org/foomo/contentserver.svg?branch=development)](https://travis-ci.org/foomo/contentserver)
[![Build Status](https://github.com/foomo/contentserver/actions/workflows/pr.yml/badge.svg?branch=main&event=push)](https://github.com/foomo/contentserver/actions/workflows/pr.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="docs/assets/Overview.svg" width="100%" height="500">
## Export Data
All you have to do is to provide a tree of content nodes as a JSON encoded RepoNode.
| Attribute | Type | Usage |
|---------------|:----------------------:|----------------------------------------------------------------------------------:|
| Id | string | unique id to identify the node |
| MimeType | string | mime-type of the node, e.g. text/html, image/png, ... |
| LinkId | string | (symbolic) link/alias to another node |
| Groups | []string | access control |
| URI | string | URI |
| Name | string | name |
| Hidden | bool | hide in menu |
| DestinationId | string | alias / symlink handling |
| Data | map[string]interface{} | payload data |
| Nodes | map[string]*RepoNode | child nodes |
| Index | []string | contains the order of of nodes |
| Attribute | Type | Usage |
|---------------|:----------------------:|------------------------------------------------------:|
| Id | string | unique id to identify the node |
| MimeType | string | mime-type of the node, e.g. text/html, image/png, ... |
| LinkId | string | (symbolic) link/alias to another node |
| Groups | []string | access control |
| URI | string | URI |
| Name | string | name |
| Hidden | bool | hide in menu |
| DestinationId | string | alias / symlink handling |
| Data | map[string]interface{} | payload data |
| Nodes | map[string]*RepoNode | child nodes |
| Index | []string | contains the order of of nodes |
### Tips
- If you do not want to build a multi-market website define a generic market, e.g. call it *universe*
- keep it lean and do not export content which should not be accessible at all, e.g. you are working on a super secret fancy new category of your website
- Hidden nodes can be resolved by their uri, but are hidden on nodes
- To avoid duplicate content provide a DestinationId ( = ContentId of the node you want to reference) instead of URIs
- If you do not want to build a multi-market website define a generic market, e.g. call it *universe*
- keep it lean and do not export content which should not be accessible at all, e.g. you are working on a super secret
fancy new category of your website
- Hidden nodes can be resolved by their uri, but are hidden on nodes
- To avoid duplicate content provide a DestinationId ( = ContentId of the node you want to reference) instead of URIs
## Request Data
There is a PHP Proxy implementation for foomo in [Foomo.ContentServer](https://github.com/foomo/Foomo.ContentServer). Feel free to use it or to implement your own proxy in the language you love. The API should be easily to implement in every other framework and language, too.
There is a PHP Proxy implementation for foomo in [Foomo.ContentServer](https://github.com/foomo/Foomo.ContentServer).
Feel free to use it or to implement your own proxy in the language you love. The API should be easily to implement in
every other framework and language, too.
## Update Flowchart
<img src="docs/assets/Update-Flow.svg" width="100%" height="700">
### Usage
```bash
$ contentserver --help
Usage of contentserver:
-address string
address to bind host:port (default "127.0.0.1:8081")
-log-level string
one of error, record, warning, notice, debug (default "record")
-var-dir string
where to put my data (default "/var/lib/contentserver")
-version
version info
$ contentserver -h
Serves content tree structures very quickly
Usage:
contentserver [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
http Start http server
socket Start socket server
version Print version information
Flags:
-h, --help help for contentserver
--log-format string log format (default "json")
--log-level string log level (default "info")
Use "contentserver [command] --help" for more information about a command.
```
## How to Contribute
Please refer to the [CONTRIBUTING](.github/CONTRIBUTING.md) details and follow the [CODE_OF_CONDUCT](.github/CODE_OF_CONDUCT.md) and [SECURITY](.github/SECURITY.md) guidelines.
## License
Copyright (c) foomo under the LGPL 3.0 license.
Distributed under LGPL 3.0 License, please see license file within the code for more details.
_Made with ♥ [foomo](https://www.foomo.org) by [bestbytes](https://www.bestbytes.com)_

16
build/buildx.Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM alpine:3.21.3
RUN apk --no-cache add ca-certificates
RUN addgroup --system --gid 1001 contentserver
RUN adduser --system --uid 1001 contentserver
COPY contentserver /usr/bin/
RUN mkdir "/var/lib/contentserver" && \
chmod 0700 "/var/lib/contentserver" && \
chown contentserver:contentserver "/var/lib/contentserver"
USER contentserver
ENTRYPOINT ["contentserver"]

View File

@ -1,155 +1,105 @@
package client
import (
"encoding/json"
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/requests"
"github.com/foomo/contentserver/responses"
"github.com/foomo/contentserver/server"
)
type serverResponse struct {
Reply interface{}
}
var (
ErrEmptyServerURL = errors.New("empty contentserver url provided")
ErrInvalidServerURL = errors.New("invalid contentserver url provided")
)
// Client a content server client
type Client struct {
Server string
conn net.Conn
t Transport
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func New(transport Transport) *Client {
return &Client{
t: transport,
}
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
// Update tell the server to update itself
func (c *Client) Update() (response *responses.Update, err error) {
response = &responses.Update{}
err = c.call(server.HandlerUpdate, &requests.Update{}, response)
return
func (c *Client) Update(ctx context.Context) (*responses.Update, error) {
type serverResponse struct {
Reply *responses.Update
}
resp := serverResponse{}
if err := c.t.Call(ctx, handler.RouteUpdate, &requests.Update{}, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
// GetContent request site content
func (c *Client) GetContent(request *requests.Content) (response *content.SiteContent, err error) {
response = &content.SiteContent{}
err = c.call(server.HandlerGetContent, request, response)
return
func (c *Client) GetContent(ctx context.Context, request *requests.Content) (*content.SiteContent, error) {
type serverResponse struct {
Reply *content.SiteContent
}
resp := serverResponse{}
if err := c.t.Call(ctx, handler.RouteGetContent, request, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
// GetURIs resolve uris for ids in a dimension
func (c *Client) GetURIs(dimension string, IDs []string) (uriMap map[string]string, err error) {
uriMap = map[string]string{}
err = c.call(
server.HandlerGetURIs,
&requests.URIs{
Dimension: dimension,
IDs: IDs,
},
&uriMap,
)
return
func (c *Client) GetURIs(ctx context.Context, dimension string, ids []string) (map[string]string, error) {
type serverResponse struct {
Reply map[string]string
}
resp := serverResponse{}
if err := c.t.Call(ctx, handler.RouteGetURIs, &requests.URIs{Dimension: dimension, IDs: ids}, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
// GetNodes request nodes
func (c *Client) GetNodes(env *requests.Env, nodes map[string]*requests.Node) (nodesResponse map[string]*content.Node, err error) {
func (c *Client) GetNodes(ctx context.Context, env *requests.Env, nodes map[string]*requests.Node) (map[string]*content.Node, error) {
r := &requests.Nodes{
Env: env,
Nodes: nodes,
}
nodesResponse = map[string]*content.Node{}
err = c.call(server.HandlerGetNodes, r, &nodesResponse)
return
type serverResponse struct {
Reply map[string]*content.Node
}
resp := serverResponse{}
if err := c.t.Call(ctx, handler.RouteGetNodes, r, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
// GetRepo get the whole repo
func (c *Client) GetRepo() (response map[string]*content.RepoNode, err error) {
response = map[string]*content.RepoNode{}
err = c.call(server.HandlerGetRepo, &requests.Repo{}, &response)
return
func (c *Client) GetRepo(ctx context.Context) (map[string]*content.RepoNode, error) {
type serverResponse struct {
Reply map[string]*content.RepoNode
}
resp := serverResponse{}
if err := c.t.Call(ctx, handler.RouteGetRepo, &requests.Repo{}, &resp); err != nil {
return nil, err
}
return resp.Reply, nil
}
// func (c *Client) closeConnection() error {
// if c.conn != nil {
// err := c.conn.Close()
// if err != nil {
// return err
// }
// c.conn = nil
// }
// return nil
// }
// func (c *Client) getConnection() (conn net.Conn, err error) {
// // we need some pooling here
// return
// }
func (c *Client) call(handler server.Handler, request interface{}, response interface{}) error {
jsonBytes, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("could not marshal request : %q", err)
}
conn, err := net.Dial("tcp", c.Server)
if err != nil {
return fmt.Errorf("can not call server - connection error: %q", err)
}
defer conn.Close()
// write header result will be like handler:2{}
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", handler, len(jsonBytes))), jsonBytes...)
// send request
written := 0
l := len(jsonBytes)
for written < l {
n, err := conn.Write(jsonBytes[written:])
if err != nil {
return fmt.Errorf("failed to send request: %q", err)
}
written += n
}
// read response
responseBytes := []byte{}
buf := make([]byte, 4096)
responseLength := 0
for {
n, err := conn.Read(buf)
if err != nil && err != io.EOF {
return fmt.Errorf("an error occured while reading the response: %q", err)
}
if n == 0 {
break
}
responseBytes = append(responseBytes, buf[0:n]...)
if responseLength == 0 {
for index, byte := range responseBytes {
if byte == 123 {
// opening bracket
responseLength, err = strconv.Atoi(string(responseBytes[0:index]))
if err != nil {
return errors.New("could not read response length: " + err.Error())
}
responseBytes = responseBytes[index:]
break
}
}
}
if responseLength > 0 && len(responseBytes) == responseLength {
break
}
}
// unmarshal response
responseJSONErr := json.Unmarshal(responseBytes, &serverResponse{Reply: response})
if responseJSONErr != nil {
// is it an error ?
remoteErr := responses.Error{}
remoteErrJSONErr := json.Unmarshal(responseBytes, remoteErr)
if remoteErrJSONErr == nil {
return remoteErr
}
return fmt.Errorf("could not unmarshal response : %q %q", remoteErrJSONErr, string(responseBytes))
}
return nil
func (c *Client) Close() {
c.t.Close()
}

View File

@ -1,145 +1,174 @@
package client
package client_test
import (
"encoding/json"
"sync"
"testing"
"time"
"github.com/foomo/contentserver/client"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/log"
"github.com/foomo/contentserver/repo/mock"
"github.com/foomo/contentserver/server"
"github.com/foomo/contentserver/pkg/repo"
"github.com/foomo/contentserver/pkg/repo/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
)
var testServerIsRunning = false
func dump(t *testing.T, v interface{}) {
jsonBytes, err := json.MarshalIndent(v, "", " ")
if err != nil {
t.Fatal("could not dump v", v, "err", err)
return
}
t.Log(string(jsonBytes))
}
func getTestClient(t testing.TB) *Client {
log.SelectedLevel = log.LevelError
addr := "127.0.0.1:9999"
if !testServerIsRunning {
testServerIsRunning = true
testServer, varDir := mock.GetMockData(t)
go server.Run(testServer.URL+"/repo-two-dimensions.json", addr, varDir)
time.Sleep(time.Millisecond * 100)
}
return &Client{
Server: addr,
}
}
func TestUpdate(t *testing.T) {
c := getTestClient(t)
response, err := c.Update()
if err != nil {
t.Fatal("unexpected err", err)
}
if !response.Success {
t.Fatal("update has to return .Sucesss true", response)
}
stats := response.Stats
if !(stats.RepoRuntime > float64(0.0)) || !(stats.OwnRuntime > float64(0.0)) {
t.Fatal("stats invalid")
}
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
t.Parallel()
response, err := c.Update(t.Context())
require.NoError(t, err)
require.True(t, response.Success, "update has to return .Sucesss true")
assert.Greater(t, response.Stats.OwnRuntime, 0.0)
assert.Greater(t, response.Stats.RepoRuntime, 0.0)
})
}
func TestGetURIs(t *testing.T) {
c := getTestClient(t)
request := mock.MakeValidURIsRequest()
uriMap, err := c.GetURIs(request.Dimension, request.IDs)
if err != nil {
t.Fatal(err)
}
if uriMap[request.IDs[0]] != "/a" {
t.Fatal(uriMap)
}
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
t.Parallel()
request := mock.MakeValidURIsRequest()
uriMap, err := c.GetURIs(t.Context(), request.Dimension, request.IDs)
time.Sleep(100 * time.Millisecond)
require.NoError(t, err)
assert.Equal(t, "/a", uriMap[request.IDs[0]])
})
}
func TestGetRepo(t *testing.T) {
c := getTestClient(t)
r, err := c.GetRepo()
if err != nil {
t.Fatal(err)
}
if r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64) != float64(1) {
t.Fatal("failed to drill deep for data")
}
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
t.Parallel()
r, err := c.GetRepo(t.Context())
require.NoError(t, err)
if assert.NotEmpty(t, r, "received empty JSON from GetRepo") {
assert.InDelta(t, 1.0, r["dimension_foo"].Nodes["id-a"].Data["baz"].(float64), 0, "failed to drill deep for data") //nolint:forcetypeassert
}
})
}
func TestGetNodes(t *testing.T) {
c := getTestClient(t)
nodesRequest := mock.MakeNodesRequest()
nodes, err := c.GetNodes(nodesRequest.Env, nodesRequest.Nodes)
if err != nil {
t.Fatal(err)
}
testNode, ok := nodes["test"]
if !ok {
t.Fatal("that should be a node")
}
testData, ok := testNode.Item.Data["foo"]
if !ok {
t.Fatal("where is foo")
}
if testData != "bar" {
t.Fatal("testData should have bennd bar not", testData)
}
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
t.Parallel()
nodesRequest := mock.MakeNodesRequest()
nodes, err := c.GetNodes(t.Context(), nodesRequest.Env, nodesRequest.Nodes)
require.NoError(t, err)
testNode, ok := nodes["test"]
if !ok {
t.Fatal("that should be a node")
}
testData, ok := testNode.Item.Data["foo"]
if !ok {
t.Fatal("where is foo")
}
if testData != "bar" {
t.Fatal("testData should have bennd bar not", testData)
}
})
}
func TestGetContent(t *testing.T) {
c := getTestClient(t)
request := mock.MakeValidContentRequest()
response, err := c.GetContent(request)
if err != nil {
t.Fatal("unexpected err", err)
}
if request.URI != response.URI {
dump(t, request)
dump(t, response)
t.Fatal("uri mismatch")
}
if response.Status != content.StatusOk {
t.Fatal("unexpected status")
testWithClients(t, func(t *testing.T, c *client.Client) {
t.Helper()
t.Parallel()
request := mock.MakeValidContentRequest()
response, err := c.GetContent(t.Context(), request)
require.NoError(t, err)
assert.Equal(t, request.URI, response.URI)
assert.Equal(t, content.StatusOk, response.Status)
})
}
func benchmarkServerAndClientGetContent(b *testing.B, numGroups, numCalls int, client GetContentClient) {
b.Helper()
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
benchmarkClientAndServerGetContent(b, numGroups, numCalls, client)
dur := time.Since(start)
totalCalls := numGroups * numCalls
b.Log("requests per second", int(float64(totalCalls)/(float64(dur)/float64(1000000000))), dur, totalCalls)
}
}
// not very meaningful yet
func BenchmarkServerAndClient(b *testing.B) {
func benchmarkClientAndServerGetContent(tb testing.TB, numGroups, numCalls int, client GetContentClient) {
tb.Helper()
var wg sync.WaitGroup
stats := make([]int, 100)
for group := 0; group < 100; group++ {
wg.Add(1)
go func(g int) {
wg.Add(numGroups)
for group := 0; group < numGroups; group++ {
go func() {
defer wg.Done()
c := getTestClient(b)
request := mock.MakeValidContentRequest()
for i := 0; i < 1000; i++ {
response, err := c.GetContent(request)
if err != nil {
b.Fatal("unexpected err", err)
for i := 0; i < numCalls; i++ {
response, err := client.GetContent(tb.Context(), request)
if err == nil {
if request.URI != response.URI {
tb.Fatal("uri mismatch")
}
if response.Status != content.StatusOk {
tb.Fatal("unexpected status")
}
}
if request.URI != response.URI {
b.Fatal("uri mismatch")
}
if response.Status != content.StatusOk {
b.Fatal("unexpected status")
}
stats[g] = i
}
}(group)
}()
}
// Wait for all HTTP fetches to complete.
wg.Wait()
b.Log(stats)
}
func testWithClients(t *testing.T, testFunc func(t *testing.T, c *client.Client)) {
t.Helper()
t.Run("http", func(t *testing.T) {
l := zaptest.NewLogger(t)
s := initHTTPRepoServer(t, l)
c := newHTTPClient(t, s)
defer func() {
s.Close()
c.Close()
}()
testFunc(t, c)
})
t.Run("socket", func(t *testing.T) {
l := zaptest.NewLogger(t)
s := initSocketRepoServer(t, l)
c := newSocketClient(t, s.Addr().String())
defer func() {
s.Close()
c.Close()
}()
testFunc(t, c)
})
}
func initRepo(tb testing.TB, l *zap.Logger) *repo.Repo {
tb.Helper()
testRepoServer, varDir := mock.GetMockData(tb)
r := repo.New(l,
testRepoServer.URL+"/repo-two-dimensions.json",
repo.NewHistory(l,
repo.HistoryWithHistoryDir(varDir),
),
)
up := make(chan bool, 1)
r.OnLoaded(func() {
up <- true
})
go r.Start(tb.Context()) //nolint:errcheck
<-up
return r
}
// func dump(t *testing.T, v interface{}) {
// t.Helper()
// jsonBytes, err := json.MarshalIndent(v, "", " ")
// if err != nil {
// t.Fatal("could not dump v", v, "err", err)
// return
// }
// t.Log(string(jsonBytes))
// }

134
client/connectionpool.go Normal file
View File

@ -0,0 +1,134 @@
package client
import (
"net"
"time"
)
type connectionPool struct {
url string
// conn net.Conn
chanConnGet chan chan net.Conn
chanConnReturn chan connReturn
chanDrainPool chan int
}
func newConnectionPool(url string, connectionPoolSize int, waitTimeout time.Duration) *connectionPool {
connPool := &connectionPool{
url: url,
chanConnGet: make(chan chan net.Conn),
chanConnReturn: make(chan connReturn),
chanDrainPool: make(chan int),
}
go connPool.run(connectionPoolSize, waitTimeout)
return connPool
}
func (c *connectionPool) run(connectionPoolSize int, waitTimeout time.Duration) {
type poolEntry struct {
busy bool
err error
conn net.Conn
}
type waitPoolEntry struct {
entryTime time.Time
chanConn chan net.Conn
}
var (
connectionPool = make(map[int]*poolEntry, connectionPoolSize)
waitPool = map[int]*waitPoolEntry{}
)
for i := 0; i < connectionPoolSize; i++ {
connectionPool[i] = &poolEntry{
conn: nil,
busy: false,
}
}
RunLoop:
for {
// fmt.Println("----------------------- run loop ------------------------")
select {
case <-c.chanDrainPool:
// fmt.Println("<-c.chanDrainPool")
for _, waitPoolEntry := range waitPool {
waitPoolEntry.chanConn <- nil
}
break RunLoop
case <-time.After(waitTimeout):
// fmt.Println("tick", len(connectionPool), len(waitPool))
// for i, poolEntry := range connectionPool {
// fmt.Println(i, poolEntry)
// }
// for i, waitPoolEntry := range waitPool {
// fmt.Println(i, waitPoolEntry)
// }
case chanReturnNextConn := <-c.chanConnGet:
// fmt.Println("chanReturnNextConn := <-c.chanConnGet:")
nextI := 0
for i := range waitPool {
if i >= nextI {
nextI = i + 1
}
}
waitPool[nextI] = &waitPoolEntry{
chanConn: chanReturnNextConn,
entryTime: time.Now(),
}
// fmt.Println("sbdy wants a new conn", nextI)
case connReturn := <-c.chanConnReturn:
// fmt.Println("connReturn := <-c.chanConnReturn:")
for _, poolEntry := range connectionPool {
if connReturn.conn == poolEntry.conn {
poolEntry.busy = false
if connReturn.err != nil {
poolEntry.err = connReturn.err
poolEntry.conn.Close()
poolEntry.conn = nil
}
}
}
}
// refill connection pool
for _, poolEntry := range connectionPool {
if poolEntry.conn == nil {
newConn, errDial := net.Dial("tcp", c.url)
poolEntry.err = errDial
poolEntry.conn = newConn
}
}
// redistribute available connections
for _, poolEntry := range connectionPool {
if len(waitPool) == 0 {
break
}
if poolEntry.err == nil && poolEntry.conn != nil && !poolEntry.busy {
for i, waitPoolEntry := range waitPool {
// fmt.Println("---------------------------> serving wait pool", i, waitPoolEntry)
poolEntry.busy = true
delete(waitPool, i)
waitPoolEntry.chanConn <- poolEntry.conn
break
}
}
}
// waitpool cleanup
var (
waitPoolLoosers = []int{}
now = time.Now()
)
for i, waitPoolEntry := range waitPool {
if now.Sub(waitPoolEntry.entryTime) > waitTimeout {
waitPoolLoosers = append(waitPoolLoosers, i)
waitPoolEntry.chanConn <- nil
}
}
for _, i := range waitPoolLoosers {
delete(waitPool, i)
}
}
c.chanDrainPool = nil
c.chanConnReturn = nil
c.chanConnGet = nil
// fmt.Println("runloop is done", waitPool)
}

105
client/httptransport.go Normal file
View File

@ -0,0 +1,105 @@
package client
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/pkg/utils"
)
type (
HTTPTransport struct {
httpClient *http.Client
endpoint string
}
HTTPTransportOption func(*HTTPTransport)
)
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
// NewHTTPTransport will create a new http transport for the given server and client.
// Caution: the provided server url is not validated!
func NewHTTPTransport(server string, opts ...HTTPTransportOption) *HTTPTransport {
inst := &HTTPTransport{
endpoint: server,
httpClient: http.DefaultClient,
}
for _, opt := range opts {
opt(inst)
}
return inst
}
// NewHTTPClient constructs a new client to talk to the contentserver.
// It returns an error if the provided url is empty or invalid.
func NewHTTPClient(url string) (c *Client, err error) {
if url == "" {
return nil, ErrEmptyServerURL
}
// validate url
if !utils.IsValidURL(url) {
return nil, ErrInvalidServerURL
}
return New(NewHTTPTransport(url)), nil
}
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func HTTPTransportWithHTTPClient(v *http.Client) HTTPTransportOption {
return func(o *HTTPTransport) {
o.httpClient = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (t *HTTPTransport) Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error {
requestBytes, errMarshal := json.Marshal(request)
if errMarshal != nil {
return errMarshal
}
req, errNewRequest := http.NewRequestWithContext(
ctx,
http.MethodPost,
t.endpoint+"/"+string(route),
bytes.NewBuffer(requestBytes),
)
if errNewRequest != nil {
return errNewRequest
}
httpResponse, errDo := t.httpClient.Do(req)
if errDo != nil {
return errDo
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode != http.StatusOK {
return errors.New("non 200 reply")
}
if httpResponse.Body == nil {
return errors.New("empty response body")
}
responseBytes, errRead := io.ReadAll(httpResponse.Body)
if errRead != nil {
return errRead
}
return json.Unmarshal(responseBytes, response)
}
func (t *HTTPTransport) Close() {
// nothing to do here
}

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

133
client/sockettransport.go Normal file
View File

@ -0,0 +1,133 @@
package client
import (
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"time"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/responses"
)
type connReturn struct {
conn net.Conn
err error
}
type SocketTransport struct {
connPool *connectionPool
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewSocketTransport(url string, connectionPoolSize int, waitTimeout time.Duration) *SocketTransport {
return &SocketTransport{
connPool: newConnectionPool(url, connectionPoolSize, waitTimeout),
}
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (t *SocketTransport) Call(ctx context.Context, route handler.Route, request interface{}, response interface{}) error {
if t.connPool.chanDrainPool == nil {
return errors.New("connection pool has been drained, client is dead")
}
jsonBytes, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("could not marshal request : %q", err)
}
netChan := make(chan net.Conn)
t.connPool.chanConnGet <- netChan
conn := <-netChan
if conn == nil {
return errors.New("could not get a connection")
}
returnConn := func(err error) {
t.connPool.chanConnReturn <- connReturn{
conn: conn,
err: err,
}
}
// write header result will be like handler:2{}
jsonBytes = append([]byte(fmt.Sprintf("%s:%d", route, len(jsonBytes))), jsonBytes...)
// send request
var (
written = 0
l = len(jsonBytes)
)
for written < l {
n, err := conn.Write(jsonBytes[written:])
if err != nil {
returnConn(err)
return fmt.Errorf("failed to send request: %q", err)
}
written += n
}
// read response
var (
responseBytes = []byte{}
buf = make([]byte, 4096)
responseLength = 0
)
for {
n, err := conn.Read(buf)
if err != nil && err != io.EOF {
returnConn(err)
return fmt.Errorf("an error occurred while reading the response: %q", err)
}
if n == 0 {
break
}
responseBytes = append(responseBytes, buf[0:n]...)
if responseLength == 0 {
for index, byte := range responseBytes {
if byte == 123 {
// opening bracket
responseLength, err = strconv.Atoi(string(responseBytes[0:index]))
if err != nil {
returnConn(err)
return errors.New("could not read response length: " + err.Error())
}
responseBytes = responseBytes[index:]
break
}
}
}
if responseLength > 0 && len(responseBytes) == responseLength {
break
}
}
// unmarshal response
errResponse := json.Unmarshal(responseBytes, response)
if errResponse != nil {
// is it an error ?
var (
remoteErr = responses.Error{}
remoteErrJSONErr = json.Unmarshal(responseBytes, &remoteErr)
)
if remoteErrJSONErr == nil {
returnConn(remoteErrJSONErr)
return remoteErr
}
return fmt.Errorf("could not unmarshal response : %q %q", remoteErrJSONErr, string(responseBytes))
}
returnConn(nil)
return nil
}
func (t *SocketTransport) Close() {
if t.connPool.chanDrainPool != nil {
t.connPool.chanDrainPool <- 1
}
}

View File

@ -0,0 +1,61 @@
package client_test
import (
"context"
"net"
"testing"
"time"
"github.com/foomo/contentserver/client"
"github.com/foomo/contentserver/pkg/handler"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"golang.org/x/net/nettest"
)
func BenchmarkSocketClientAndServerGetContent(b *testing.B) {
l := zaptest.NewLogger(b)
socketServer := initSocketRepoServer(b, l)
socketClient := newSocketClient(b, socketServer.Addr().String())
defer socketClient.Close()
defer socketServer.Close()
benchmarkServerAndClientGetContent(b, 30, 100, socketClient)
}
func newSocketClient(tb testing.TB, address string) *client.Client {
tb.Helper()
return client.New(client.NewSocketTransport(address, 25, 100*time.Millisecond))
}
func initSocketRepoServer(tb testing.TB, l *zap.Logger) net.Listener {
tb.Helper()
r := initRepo(tb, l)
h := handler.NewSocket(l, r)
// listen on socket
ln, err := nettest.NewLocalListener("tcp")
require.NoError(tb, err)
go func() {
for {
// this blocks until connection or error
conn, err := ln.Accept()
if errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
return
} else if err != nil {
tb.Error("runSocketServer: could not accept connection", err.Error())
continue
}
// a goroutine handles conn so that the loop can accept other connections
go func() {
// l.Debug("accepted connection", zap.String("source", conn.RemoteAddr().String()))
h.Serve(conn)
}()
}
}()
return ln
}

12
client/transport.go Normal file
View File

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

126
cmd/flags.go Normal file
View File

@ -0,0 +1,126 @@
package cmd
import (
"time"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
func logLevelFlag(v *viper.Viper) string {
return v.GetString("log.level")
}
func addLogLevelFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.String("log-level", "info", "log level")
_ = v.BindPFlag("log.level", flags.Lookup("log-level"))
_ = v.BindEnv("log.level", "LOG_LEVEL")
}
func logFormatFlag(v *viper.Viper) string {
return v.GetString("log.format")
}
func addLogFormatFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.String("log-format", "json", "log format")
_ = v.BindPFlag("log.format", flags.Lookup("log-format"))
_ = v.BindEnv("log.format", "LOG_FORMAT")
}
func addressFlag(v *viper.Viper) string {
return v.GetString("address")
}
func addAddressFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.String("address", ":8080", "Address to bind to (host:port)")
_ = v.BindPFlag("address", flags.Lookup("address"))
_ = v.BindEnv("address", "CONTENT_SERVER_ADDRESS")
}
func basePathFlag(v *viper.Viper) string {
return v.GetString("base_path")
}
func addBasePathFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.String("base-path", "/contentserver", "Base path to export the webserver on")
_ = v.BindPFlag("base_path", flags.Lookup("base-path"))
_ = v.BindEnv("base_path", "CONTENT_SERVER_BASE_PATH")
}
func pollFlag(v *viper.Viper) bool {
return v.GetBool("poll.enabled")
}
func addPollFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Bool("poll", false, "If true, the address arg will be used to periodically poll the content url")
_ = v.BindPFlag("poll.enabled", flags.Lookup("poll"))
_ = v.BindEnv("poll.enabled", "CONTENT_SERVER_POLL")
}
func pollIntevalFlag(v *viper.Viper) time.Duration {
return v.GetDuration("poll.interval")
}
func addPollIntervalFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Duration("poll-interval", time.Minute, "Specifies the poll interval")
_ = v.BindPFlag("poll.interval", flags.Lookup("poll-interval"))
_ = v.BindEnv("poll.interval", "CONTENT_SERVER_POLL_INTERVAL")
}
func historyDirFlag(v *viper.Viper) string {
return v.GetString("history.dir")
}
func addHistoryDirFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.String("history-dir", "/var/lib/contentserver", "Where to put my data")
_ = v.BindPFlag("history.dir", flags.Lookup("history-dir"))
_ = v.BindEnv("history.dir", "CONTENT_SERVER_HISTORY_DIR")
}
func historyLimitFlag(v *viper.Viper) int {
return v.GetInt("history.limit")
}
func addHistoryLimitFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Int("history-limit", 2, "Number of history records to keep")
_ = v.BindPFlag("history.limit", flags.Lookup("history-limit"))
_ = v.BindEnv("history.limit", "CONTENT_SERVER_HISTORY_LIMIT")
}
func gracefulPeriodFlag(v *viper.Viper) time.Duration {
return v.GetDuration("graceful.period")
}
func addShutdownTimeoutFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Duration("graceful-period", 0, "Graceful shutdown period")
_ = v.BindPFlag("graceful.period", flags.Lookup("graceful-period"))
_ = v.BindEnv("graceful.period", "CONTENT_SERVER_GRACEFULE_PERIOD")
}
func serviceHealthzEnabledFlag(v *viper.Viper) bool {
return v.GetBool("service.healthz.enabled")
}
func addServiceHealthzEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Bool("service-healthz-enabled", false, "Enable healthz service")
_ = v.BindPFlag("service.healthz.enabled", flags.Lookup("service-healthz-enabled"))
}
func servicePrometheusEnabledFlag(v *viper.Viper) bool {
return v.GetBool("service.prometheus.enabled")
}
func addServicePrometheusEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Bool("service-prometheus-enabled", false, "Enable prometheus service")
_ = v.BindPFlag("service.prometheus.enabled", flags.Lookup("service-prometheus-enabled"))
}
func otelEnabledFlag(v *viper.Viper) bool {
return v.GetBool("otel.enabled")
}
func addOtelEnabledFlag(flags *pflag.FlagSet, v *viper.Viper) {
flags.Bool("otel-enabled", false, "Enable otel service")
_ = v.BindPFlag("otel.enabled", flags.Lookup("otel-enabled"))
_ = v.BindEnv("otel.enabled", "OTEL_ENABLED")
}

100
cmd/http.go Normal file
View File

@ -0,0 +1,100 @@
package cmd
import (
"context"
"errors"
"github.com/foomo/contentserver/pkg/handler"
"github.com/foomo/contentserver/pkg/repo"
"github.com/foomo/keel"
"github.com/foomo/keel/healthz"
keelhttp "github.com/foomo/keel/net/http"
"github.com/foomo/keel/net/http/middleware"
"github.com/foomo/keel/service"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewHTTPCommand() *cobra.Command {
v := newViper()
cmd := &cobra.Command{
Use: "http <url>",
Short: "Start http server",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var comps []string
if len(args) == 0 {
comps = cobra.AppendActiveHelp(comps, "You must specify the URL for the repository you are adding")
} else {
comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments")
}
return comps, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
svr := keel.NewServer(
keel.WithHTTPPrometheusService(servicePrometheusEnabledFlag(v)),
keel.WithHTTPHealthzService(serviceHealthzEnabledFlag(v)),
keel.WithPrometheusMeter(servicePrometheusEnabledFlag(v)),
keel.WithGracefulPeriod(gracefulPeriodFlag(v)),
keel.WithOTLPGRPCTracer(otelEnabledFlag(v)),
)
l := svr.Logger()
r := repo.New(l.Named("inst.repo"),
args[0],
repo.NewHistory(l.Named("inst.history"),
repo.HistoryWithHistoryDir(historyDirFlag(v)),
repo.HistoryWithHistoryLimit(historyLimitFlag(v)),
),
repo.WithHTTPClient(
keelhttp.NewHTTPClient(
keelhttp.HTTPClientWithTelemetry(),
),
),
repo.WithPollInterval(pollIntevalFlag(v)),
repo.WithPoll(pollFlag(v)),
)
isLoadedHealtherFn := healthz.NewHealthzerFn(func(ctx context.Context) error {
if !r.Loaded() {
return errors.New("repo not loaded yet")
}
return nil
})
// start initial update and handle error
svr.AddStartupHealthzers(isLoadedHealtherFn)
svr.AddReadinessHealthzers(isLoadedHealtherFn)
svr.AddServices(
service.NewGoRoutine(l.Named("go.repo"), "repo", func(ctx context.Context, l *zap.Logger) error {
return r.Start(ctx)
}),
service.NewHTTP(l.Named("svc.http"), "http", addressFlag(v),
handler.NewHTTP(l.Named("inst.handler"), r, handler.WithBasePath(basePathFlag(v))),
middleware.Telemetry(),
middleware.Logger(),
middleware.GZip(),
middleware.Recover(),
),
)
svr.Run()
return nil
},
}
flags := cmd.Flags()
addAddressFlag(flags, v)
addBasePathFlag(flags, v)
addPollFlag(flags, v)
addPollIntervalFlag(flags, v)
addHistoryDirFlag(flags, v)
addHistoryLimitFlag(flags, v)
addShutdownTimeoutFlag(flags, v)
addOtelEnabledFlag(flags, v)
addServiceHealthzEnabledFlag(flags, v)
addServicePrometheusEnabledFlag(flags, v)
return cmd
}

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

@ -4,6 +4,6 @@ package content
const (
// Indent for json indentation
Indent string = "\t"
// PathSeparator seprator for paths in URIs
// PathSeparator separator for paths in URIs
PathSeparator = "/"
)

View File

@ -6,7 +6,9 @@ type Item struct {
Name string `json:"name"`
URI string `json:"URI"`
MimeType string `json:"mimeType"`
Hidden bool `json:"hidden,omitempty"`
Data map[string]interface{} `json:"data"`
Groups []string `json:"groups"`
}
// NewItem item contructor

View File

@ -22,26 +22,26 @@ type RepoNode struct {
// published from - to is going to be an array of fromTos
}
// NewRepoNode constructor
func NewRepoNode() *RepoNode {
return &RepoNode{
Data: make(map[string]interface{}),
Nodes: make(map[string]*RepoNode),
}
}
// // NewRepoNode constructor
// func NewRepoNode() *RepoNode {
// return &RepoNode{
// Data: make(map[string]interface{}, 0), // set initial size to zero explicitly?
// Nodes: make(map[string]*RepoNode, 0),
// }
// }
// WireParents helper method to reference from child to parent in a tree
// recursively
func (node *RepoNode) WireParents() {
for _, childNode := range node.Nodes {
childNode.parent = node
func (n *RepoNode) WireParents() {
for _, childNode := range n.Nodes {
childNode.parent = n
childNode.WireParents()
}
}
// InPath is the given node in a path
func (node *RepoNode) InPath(path []*Item) bool {
myParentID := node.parent.ID
func (n *RepoNode) InPath(path []*Item) bool {
myParentID := n.parent.ID
for _, pathItem := range path {
if pathItem.ID == myParentID {
return true
@ -51,57 +51,73 @@ func (node *RepoNode) InPath(path []*Item) bool {
}
// GetPath get a path for a repo node
func (node *RepoNode) GetPath() []*Item {
parentNode := node.parent
pathLength := 0
func (n *RepoNode) GetPath(dataFields []string) []*Item {
var (
parentNode = n.parent
pathLength = 0
)
for parentNode != nil {
parentNode = parentNode.parent
pathLength++
}
parentNode = node.parent
i := 0
path := make([]*Item, pathLength)
parentNode = n.parent
var (
i = 0
path = make([]*Item, pathLength)
)
if dataFields == nil {
dataFields = []string{}
}
for parentNode != nil {
path[i] = parentNode.ToItem([]string{})
path[i] = parentNode.ToItem(dataFields)
parentNode = parentNode.parent
i++
}
return path
}
// ToItem convert a re po node to a simple repo item
func (node *RepoNode) ToItem(dataFields []string) *Item {
// ToItem convert a repo node to a simple repo item
func (n *RepoNode) ToItem(dataFields []string) *Item {
item := NewItem()
item.ID = node.ID
item.Name = node.Name
item.MimeType = node.MimeType
item.URI = node.URI
for _, dataField := range dataFields {
if data, ok := node.Data[dataField]; ok {
item.Data[dataField] = data
item.ID = n.ID
item.Name = n.Name
item.MimeType = n.MimeType
item.Hidden = n.Hidden
item.URI = n.URI
item.Groups = n.Groups
if dataFields == nil {
item.Data = n.Data
} else {
for _, dataField := range dataFields {
if data, ok := n.Data[dataField]; ok {
item.Data[dataField] = data
}
}
}
return item
}
// GetParent get the parent node of a node
func (node *RepoNode) GetParent() *RepoNode {
return node.parent
func (n *RepoNode) GetParent() *RepoNode {
return n.parent
}
// AddNode adds a named child node
func (node *RepoNode) AddNode(name string, childNode *RepoNode) *RepoNode {
node.Nodes[name] = childNode
return node
func (n *RepoNode) AddNode(name string, childNode *RepoNode) *RepoNode {
n.Nodes[name] = childNode
return n
}
// IsOneOfTheseMimeTypes is the node one of the given mime types
func (node *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
func (n *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
if len(mimeTypes) == 0 {
return true
}
for _, mimeType := range mimeTypes {
if mimeType == node.MimeType {
if mimeType == n.MimeType {
return true
}
}
@ -110,14 +126,14 @@ func (node *RepoNode) IsOneOfTheseMimeTypes(mimeTypes []string) bool {
// CanBeAccessedByGroups can this node be accessed by at least one the given
// groups
func (node *RepoNode) CanBeAccessedByGroups(groups []string) bool {
func (n *RepoNode) CanBeAccessedByGroups(groups []string) bool {
// no groups set on node => anybody can access it
if len(node.Groups) == 0 {
if len(n.Groups) == 0 {
return true
}
for _, group := range groups {
for _, myGroup := range node.Groups {
for _, myGroup := range n.Groups {
if group == myGroup {
return true
}
@ -127,10 +143,10 @@ func (node *RepoNode) CanBeAccessedByGroups(groups []string) bool {
}
// PrintNode essentially a recursive dump
func (node *RepoNode) PrintNode(id string, level int) {
func (n *RepoNode) PrintNode(id string, level int) {
prefix := strings.Repeat(Indent, level)
fmt.Printf("%s %s %s:\n", prefix, id, node.Name)
for key, childNode := range node.Nodes {
fmt.Printf("%s %s %s:\n", prefix, id, n.Name)
for key, childNode := range n.Nodes {
childNode.PrintNode(key, level+1)
}
}

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,79 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/foomo/contentserver/log"
"github.com/foomo/contentserver/server"
)
const (
logLevelDebug = "debug"
logLevelNotice = "notice"
logLevelWarning = "warning"
logLevelRecord = "record"
logLevelError = "error"
)
var (
uniqushPushVersion = "content-server 1.3.4"
showVersionFlag = flag.Bool("version", false, "version info")
address = flag.String("address", "127.0.0.1:8081", "address to bind host:port")
varDir = flag.String("var-dir", "/var/lib/contentserver", "where to put my data")
logLevelOptions = []string{
logLevelError,
logLevelRecord,
logLevelWarning,
logLevelNotice,
logLevelDebug,
}
logLevel = flag.String(
"log-level",
logLevelRecord,
fmt.Sprintf(
"one of %s",
strings.Join(logLevelOptions, ", "),
),
)
)
func exitUsage(code int) {
fmt.Printf("Usage: %s http(s)://your-content-server/path/to/content.json\n", os.Args[0])
flag.PrintDefaults()
os.Exit(code)
}
func main() {
flag.Parse()
if *showVersionFlag {
fmt.Printf("%v\n", uniqushPushVersion)
return
}
if len(flag.Args()) == 1 {
fmt.Println(*address, flag.Arg(0))
level := log.LevelRecord
switch *logLevel {
case logLevelError:
level = log.LevelError
case logLevelRecord:
level = log.LevelRecord
case logLevelWarning:
level = log.LevelWarning
case logLevelNotice:
level = log.LevelNotice
case logLevelDebug:
level = log.LevelDebug
}
log.SelectedLevel = level
err := server.Run(flag.Arg(0), *address, *varDir)
if err != nil {
fmt.Println("exiting with error", err)
os.Exit(1)
}
} else {
exitUsage(1)
}
}

BIN
contentserver.graffle Normal file

Binary file not shown.

View File

@ -0,0 +1,327 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="133 101 1838 2500" width="1838" height="2500">
<defs>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
<font-face-src>
<font-face-name name="HelveticaNeue"/>
</font-face-src>
</font-face>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
<font-face-src>
<font-face-name name="HelveticaNeue-Bold"/>
</font-face-src>
</font-face>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_2" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="#ff2600">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker_3" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="#ff2600">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
<font-face font-family="Futura" font-size="80" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
</defs>
<metadata> Produced by OmniGraffle 7.10.2
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
</metadata>
<g id="Horizontal_Update" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
<title>Horizontal Update</title>
<rect fill="white" x="133" y="101" width="1838" height="2500"/>
<g id="Horizontal_Update: Layer 1">
<title>Layer 1</title>
<g id="Graphic_4">
<rect x="526" y="282" width="539" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(531 303.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="209.828" y="15">*Repo.Update()</tspan>
</text>
</g>
<g id="Graphic_5">
<rect x="526" y="425" width="539" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(531 446.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="200.644" y="15">*Repo.tryUpdate()</tspan>
</text>
</g>
<g id="Graphic_7">
<rect x="134" y="589" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(139 610.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="23.344" y="15">if updateErr != errUpdateRejected</tspan>
</text>
</g>
<g id="Line_8">
<line x1="693.9375" y1="489" x2="396.84877" y2="584.0684" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_12">
<rect x="134" y="770" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(139 791.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="43.848" y="15">*Repo.tryToRestoreCurrent()</tspan>
</text>
</g>
<g id="Line_13">
<line x1="283" y1="653" x2="283" y2="756.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_15">
<line x1="795.5" y1="346" x2="795.5" y2="411.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_16">
<rect x="1137" y="589" width="473" height="63" fill="white"/>
<rect x="1137" y="589" width="473" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1142 610.5)" fill="#ff2600">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="162.756" y="15">*Repo.history.lock()</tspan>
</text>
</g>
<g id="Graphic_18">
<rect x="828" y="589" width="222" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(833 610.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="15.032" y="15">return errUpdateRejected</tspan>
</text>
</g>
<g id="Line_20">
<line x1="823.9375" y1="489" x2="902.0678" y2="578.29176" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_21">
<line x1="910.0427" y1="489" x2="1246.5472" y2="584.4788" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_22">
<rect x="824.0671" y="519.599" width="81.28906" height="32" fill="white"/>
<text transform="translate(829.0671 524.599)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">queue full</tspan>
</text>
</g>
<g id="Graphic_23">
<rect x="1033.0312" y="521.1068" width="93.11719" height="32" fill="white"/>
<text transform="translate(1038.0312 526.1068)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="2.46875" y="17">queue free </tspan>
</text>
</g>
<g id="Graphic_24">
<rect x="134" y="966" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(139 987.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="59.832" y="15">return updateResponse</tspan>
</text>
</g>
<g id="Line_25">
<line x1="283" y1="834" x2="283" y2="952.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_28">
<rect x="982.5" y="924" width="782" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(987.5 945.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="299.496" y="16">*Repo.updateRoutine()</tspan>
</text>
</g>
<g id="Graphic_29">
<rect x="982.5" y="987" width="782" height="1072" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_31">
<rect x="1013.5" y="1023" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1018.5 1044.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="13.305457" y="15">resChan &lt;- *Repo.updateInProgressChannel:</tspan>
</text>
</g>
<g id="Graphic_32">
<rect x="1062.5" y="1110" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1067.5 1131.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.24146" y="15">*Repo.update()</tspan>
</text>
</g>
<g id="Graphic_33">
<rect x="1124.9145" y="1197" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1129.9145 1218.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="134.88146" y="15">*Repo.get()</tspan>
</text>
</g>
<g id="Graphic_34">
<rect x="1195.5" y="1284" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1200.5 1305.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="67.75346" y="15">*Repo.loadNodesFromJSON()</tspan>
</text>
</g>
<g id="Graphic_35">
<rect x="1259.5" y="1371" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1264.5 1392.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.47346" y="15">*Repo.loadNodes()</tspan>
</text>
</g>
<g id="Line_36">
<line x1="1373.5" y1="826" x2="1373.5" y2="910.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
</g>
<g id="Graphic_37">
<rect x="1302.5" y="1458" width="409.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1307.5 1461.828)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="15">for dimension, newNode := range nodes {</tspan>
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="33.448"> *Repo.updateDimension(dimension, newNode)</tspan>
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="51.895996">}</tspan>
</text>
</g>
<g id="Graphic_38">
<rect x="1352.5" y="1549" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1357.5 1570.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="47.369457" y="16">*Repo.dimensionUpdateRoutine()</tspan>
</text>
</g>
<g id="Graphic_39">
<rect x="1352.5" y="1612" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1357.5 1633.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="79.44946" y="15">*Repo._updateDimension()</tspan>
</text>
</g>
<g id="Graphic_40">
<rect x="1352.5" y="1675" width="359.1709" height="343" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_41">
<rect x="1373.5" y="1689" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1378.5 1710.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="60.152" y="15">newNode.WireParents()</tspan>
</text>
</g>
<g id="Graphic_42">
<rect x="1373.5" y="1772.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1378.5 1794)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="90.072" y="15">buildDirectory()</tspan>
</text>
</g>
<g id="Graphic_43">
<rect x="1373.5" y="1856" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1378.5 1877.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="99.872" y="15">wireAliases()</tspan>
</text>
</g>
<g id="Graphic_44">
<rect x="1373.5" y="1939.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1378.5 1961)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="20.8" y="15">dimensionUpdateDoneChan &lt;- err</tspan>
</text>
</g>
<g id="Graphic_45">
<rect x="982.5" y="2186" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(987.5 2207.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="71.76146" y="15">*Repo.history.add(jsonBytes)</tspan>
</text>
</g>
<g id="Line_46">
<line x1="1210.0304" y1="2060" x2="1175.7356" y2="2172.6591" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
</g>
<g id="Graphic_48">
<rect x="982.5" y="2537" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(987.5 2558.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="62.79346" y="15">resultChan &lt;- updateResponse</tspan>
</text>
</g>
<g id="Line_47">
<line x1="1162.0855" y1="2488" x2="1162.0855" y2="2523.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_49">
<line x1="1265.4012" y1="1174" x2="1273.6646" y2="1185.5183" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_50">
<line x1="1330.8681" y1="1261" x2="1340.5898" y2="1272.9824" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_51">
<line x1="1398.9935" y1="1348" x2="1407.5333" y2="1359.6088" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_52">
<line x1="1464.4878" y1="1435" x2="1473.739" y2="1446.8363" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_53">
<line x1="1522.5" y1="1753" x2="1522.5" y2="1758.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_54">
<line x1="1522.5" y1="1836.5" x2="1522.5" y2="1842.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_55">
<line x1="1522.5" y1="1920" x2="1522.5" y2="1925.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_57">
<rect x="485.6393" y="523.2829" width="102.32812" height="32" fill="white"/>
<text transform="translate(490.6393 528.2829)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">update error</tspan>
</text>
</g>
<g id="Graphic_60">
<rect x="519" y="589" width="222" height="63" fill="white"/>
<rect x="519" y="589" width="222" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(524 610.5)" fill="#ff2600">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="15.032" y="15">return errUpdateRejected</tspan>
</text>
</g>
<g id="Line_59">
<line x1="762.7027" y1="489" x2="671.9604" y2="578.91994" marker-end="url(#FilledArrow_Marker_2)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_58">
<rect x="663.3299" y="519.599" width="104.69531" height="32" fill="white"/>
<text transform="translate(668.3299 524.599)" fill="#ff2600">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="#ff2600" x="0" y="17">lockfile exists</tspan>
</text>
</g>
<g id="Graphic_61">
<rect x="1137" y="762" width="473" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1142 783.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="8.42" y="15">*Repo.updateInProgressChan &lt;- make(chan updateResponse)</tspan>
</text>
</g>
<g id="Line_62">
<line x1="1373.5" y1="653" x2="1373.5" y2="748.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
</g>
<g id="Graphic_63">
<rect x="982.5" y="2305" width="359.1709" height="63" fill="white"/>
<rect x="982.5" y="2305" width="359.1709" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(987.5 2326.5)" fill="#ff2600">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="96.94546" y="15">*Repo.history.unlock()</tspan>
</text>
</g>
<g id="Line_64">
<line x1="1162.0855" y1="2250" x2="1162.0855" y2="2291.1" marker-end="url(#FilledArrow_Marker_2)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_66">
<rect x="982.5" y="2424" width="359.1709" height="63" fill="white"/>
<rect x="982.5" y="2424" width="359.1709" height="63" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(987.5 2445.5)" fill="#ff2600">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" x="58.27346" y="15">*Repo.history.</tspan>
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="#ff2600" y="15">broadcastUpdate()</tspan>
</text>
</g>
<g id="Line_67">
<line x1="1162.0855" y1="2369" x2="1162.0855" y2="2410.1" marker-end="url(#FilledArrow_Marker_3)" stroke="#ff2600" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_68">
<text transform="translate(139 106.61719)" fill="black">
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" x="0" y="83">Contentserver Horizontal Scaling: Update Flow</tspan>
</text>
</g>
<g id="Graphic_69">
<path d="M 1341.6709 2305 L 1700.8418 2305 L 1700.8418 2368 L 1341.6709 2368 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
<text transform="translate(1346.6709 2326.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.84146" y="15">Remove lockfile</tspan>
</text>
</g>
<g id="Graphic_70">
<path d="M 1341.6709 2424 L 1700.8418 2424 L 1700.8418 2487 L 1341.6709 2487 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
<text transform="translate(1346.6709 2445.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="76.04946" y="15">Broadcast update via NATS</tspan>
</text>
</g>
<g id="Graphic_71">
<path d="M 1610 589 L 1969.171 589 L 1969.171 652 L 1610 652 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="2.0,8.0" stroke-width="2"/>
<text transform="translate(1615 610.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="123.62546" y="15">Create lockfile</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

391
docs/assets/Horizontal.svg Normal file
View File

@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-263 -474 2433 1740" width="2433" height="1740">
<defs>
<font-face font-family="Helvetica Neue" font-size="25" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
<font-face-src>
<font-face-name name="HelveticaNeue-Bold"/>
</font-face-src>
</font-face>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
<font-face-src>
<font-face-name name="HelveticaNeue"/>
</font-face-src>
</font-face>
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
</defs>
<metadata> Produced by OmniGraffle 7.10.2
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
</metadata>
<g id="Horizontal" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
<title>Horizontal</title>
<rect fill="white" x="-263" y="-474" width="2433" height="1740"/>
<g id="Horizontal: Layer 1">
<title>Layer 1</title>
<g id="Graphic_35">
<rect x="-65" y="991" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_32">
<rect x="775" y="984" width="554" height="231" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_20">
<rect x="-65" y="195" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_36">
<rect x="-262" y="342" width="384" height="408" fill="white"/>
<rect x="-262" y="342" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_21">
<rect x="465" y="361.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_2">
<rect x="-65" y="121.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-60 142.75)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="390.1" y="24">contentserver</tspan>
</text>
</g>
<g id="Graphic_3">
<rect x="140.5" y="425.4524" width="306" height="84" fill="white"/>
<rect x="140.5" y="425.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(145.5 457.4524)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
</text>
</g>
<g id="Graphic_4">
<rect x="140.5" y="567.5476" width="306" height="84" fill="white"/>
<rect x="140.5" y="567.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(145.5 599.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
</text>
</g>
<g id="Graphic_5">
<rect x="886" y="-368.5" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(891 -336.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="87.992" y="15">CONTENT.JSON</tspan>
</text>
</g>
<g id="Graphic_6">
<rect x="847" y="-473" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(852 -455)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="21.475" y="24">Content Source Webservice</tspan>
</text>
</g>
<g id="Graphic_7">
<rect x="465" y="285" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(470 307.9499)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="155.525" y="24">Repo</tspan>
</text>
</g>
<g id="Graphic_9">
<rect x="670" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 430.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_11">
<rect x="495" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 430.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_22">
<rect x="670" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 543.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_23">
<rect x="495" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 543.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_24">
<rect x="847" y="-406" width="384" height="162.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_25">
<rect x="-223" y="446.06663" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="446.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 463.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
</text>
</g>
<g id="Graphic_26">
<rect x="-223" y="379" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="379" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 396.48096)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
</text>
</g>
<g id="Graphic_27">
<rect x="-223" y="513.13327" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="513.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 530.6142)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
</text>
</g>
<g id="Graphic_28">
<rect x="-223" y="580.1999" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="580.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 597.68086)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
</text>
</g>
<g id="Graphic_29">
<rect x="-223" y="650.5381" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="650.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 668.019)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
</text>
</g>
<g id="Graphic_30">
<rect x="831.2656" y="1071" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(836.2656 1084)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="102.52637" y="15">contentserver-repo-current.json</tspan>
</text>
</g>
<g id="Graphic_31">
<rect x="775" y="917" width="554" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(780 935)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="115.975" y="24">Var Directory (aka History)</tspan>
</text>
</g>
<g id="Graphic_34">
<rect x="-65" y="924" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-60 942)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="36.7875" y="24">Logfile (JSON or TXT)</tspan>
</text>
</g>
<g id="Graphic_37">
<rect x="-262" y="275" width="384" height="67" fill="white"/>
<rect x="-262" y="275" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-257 293)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="166.4125" y="24">API</tspan>
</text>
</g>
<g id="Graphic_39">
<rect x="670" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 657.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Graphic_40">
<rect x="495" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 657.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Graphic_43">
<rect x="831.2656" y="1011.5" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(836.2656 1024.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="97.70237" y="15">contentserver-repo-[Timestamp].json</tspan>
</text>
</g>
<g id="Graphic_45">
<rect x="-42" y="1011.5" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-37 1023.5)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_44">
<rect x="-42" y="1071" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-37 1083)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_46">
<rect x="-42" y="1130.5" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-37 1142.5)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_47">
<rect x="-42" y="1190" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-37 1202)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Line_48">
<line x1="461.9265" y1="120.5" x2="922.1852" y2="-234.61976" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_56">
<line x1="202.23013" y1="815" x2="136.16397" y2="912.3267" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_57">
<line x1="857.8643" y1="815" x2="991.9924" y2="908.6168" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_59">
<rect x="1213" y="187" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_60">
<rect x="1016" y="334" width="384" height="408" fill="white"/>
<rect x="1016" y="334" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_61">
<rect x="1743" y="353.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_62">
<rect x="1213" y="113.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1218 134.75)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="390.1" y="24">contentserver</tspan>
</text>
</g>
<g id="Graphic_63">
<rect x="1418.5" y="417.4524" width="306" height="84" fill="white"/>
<rect x="1418.5" y="417.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1423.5 449.4524)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
</text>
</g>
<g id="Graphic_64">
<rect x="1418.5" y="559.5476" width="306" height="84" fill="white"/>
<rect x="1418.5" y="559.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1423.5 591.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
</text>
</g>
<g id="Graphic_65">
<rect x="1743" y="277" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1748 299.9499)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="155.525" y="24">Repo</tspan>
</text>
</g>
<g id="Graphic_66">
<rect x="1948" y="388.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1953 422.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_67">
<rect x="1773" y="388.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1778 422.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_68">
<rect x="1948" y="501.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1953 535.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_69">
<rect x="1773" y="501.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1778 535.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_70">
<rect x="1055" y="438.06663" width="306" height="54.961924" fill="white"/>
<rect x="1055" y="438.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1060 455.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
</text>
</g>
<g id="Graphic_71">
<rect x="1055" y="371" width="306" height="54.961924" fill="white"/>
<rect x="1055" y="371" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1060 388.48096)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
</text>
</g>
<g id="Graphic_72">
<rect x="1055" y="505.13327" width="306" height="54.961924" fill="white"/>
<rect x="1055" y="505.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1060 522.6142)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
</text>
</g>
<g id="Graphic_73">
<rect x="1055" y="572.1999" width="306" height="54.961924" fill="white"/>
<rect x="1055" y="572.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1060 589.68086)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
</text>
</g>
<g id="Graphic_74">
<rect x="1055" y="642.5381" width="306" height="54.961924" fill="white"/>
<rect x="1055" y="642.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1060 660.019)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
</text>
</g>
<g id="Graphic_75">
<rect x="1016" y="267" width="384" height="67" fill="white"/>
<rect x="1016" y="267" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1021 285)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="166.4125" y="24">API</tspan>
</text>
</g>
<g id="Graphic_76">
<rect x="1948" y="615.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1953 649.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Graphic_77">
<rect x="1773" y="615.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1778 649.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Line_78">
<line x1="1903.2618" y1="807" x2="1967.6353" y2="901.1666" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_79">
<line x1="1253.9747" y1="807" x2="1111.0744" y2="908.5285" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_84">
<line x1="1639.1832" y1="112.5" x2="1162.3254" y2="-234.90403" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_85">
<rect x="1828" y="979.816" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_86">
<rect x="1828" y="912.816" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1833 930.816)" fill="black">
<tspan font-family="Helvetica Neue" font-size="25" font-weight="700" fill="black" x="36.7875" y="24">Logfile (JSON or TXT)</tspan>
</text>
</g>
<g id="Graphic_87">
<rect x="1851" y="1000.316" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1856 1012.316)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_88">
<rect x="1851" y="1059.816" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1856 1071.816)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_89">
<rect x="1851" y="1119.316" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1856 1131.316)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_90">
<rect x="1851" y="1178.816" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1856 1190.816)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_91">
<rect x="831.2656" y="1137" width="441.46875" height="46" fill="#ccc"/>
<rect x="831.2656" y="1137" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(836.2656 1150)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="135.42237" y="15">updateInProgress.lock</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

250
docs/assets/Overview.svg Normal file
View File

@ -0,0 +1,250 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-549 -177 1441 1431" width="1441" height="1431">
<defs>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
<font-face-src>
<font-face-name name="HelveticaNeue-Bold"/>
</font-face-src>
</font-face>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
<font-face-src>
<font-face-name name="HelveticaNeue"/>
</font-face-src>
</font-face>
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
</defs>
<metadata> Produced by OmniGraffle 7.10.2
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
</metadata>
<g id="Overview" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
<title>Overview</title>
<rect fill="white" x="-549" y="-177" width="1441" height="1431"/>
<g id="Overview: Layer 1">
<title>Layer 1</title>
<g id="Graphic_35">
<rect x="546" y="979.7753" width="341" height="273" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_32">
<rect x="-69" y="979.816" width="554" height="185.45933" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_20">
<rect x="-65" y="195" width="956" height="619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_36">
<rect x="-262" y="342" width="384" height="408" fill="white"/>
<rect x="-262" y="342" width="384" height="408" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_21">
<rect x="465" y="361.8998" width="384" height="398.1002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_2">
<rect x="-65" y="121.5" width="956" height="73.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-60 148.25)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="419.944" y="16">contentserver</tspan>
</text>
</g>
<g id="Graphic_3">
<rect x="140.5" y="425.4524" width="306" height="84" fill="white"/>
<rect x="140.5" y="425.4524" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(145.5 457.4524)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.864" y="15">Web server</tspan>
</text>
</g>
<g id="Graphic_4">
<rect x="140.5" y="567.5476" width="306" height="84" fill="white"/>
<rect x="140.5" y="567.5476" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(145.5 599.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="81.04" y="15">TCP Socket server</tspan>
</text>
</g>
<g id="Graphic_5">
<rect x="260" y="-71.5" width="306" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(265 -39.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="87.992" y="15">CONTENT.JSON</tspan>
</text>
</g>
<g id="Graphic_6">
<rect x="221" y="-176" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(226 -152.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="81.064" y="16">Content Source Webservice</tspan>
</text>
</g>
<g id="Graphic_7">
<rect x="465" y="285" width="384" height="76.89978" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(470 313.4499)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="166.856" y="16">Repo</tspan>
</text>
</g>
<g id="Graphic_9">
<rect x="670" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 430.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_11">
<rect x="495" y="396.94934" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 430.89207)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_22">
<rect x="670" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 543.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.48" y="15">RepoNode</tspan>
</text>
</g>
<g id="Graphic_23">
<rect x="495" y="509.94493" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 543.88767)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="35.208" y="15">Dimension</tspan>
</text>
</g>
<g id="Graphic_24">
<rect x="221" y="-109" width="384" height="162.5" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_25">
<rect x="-223" y="446.06663" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="446.06663" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 463.5476)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="106.512" y="15">GetContent</tspan>
</text>
</g>
<g id="Graphic_26">
<rect x="-223" y="379" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="379" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 396.48096)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="117.784" y="15">GetURIs</tspan>
</text>
</g>
<g id="Graphic_27">
<rect x="-223" y="513.13327" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="513.13327" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 530.6142)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="111.704" y="15">GetNodes</tspan>
</text>
</g>
<g id="Graphic_28">
<rect x="-223" y="580.1999" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="580.1999" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 597.68086)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.624" y="15">Update</tspan>
</text>
</g>
<g id="Graphic_29">
<rect x="-223" y="650.5381" width="306" height="54.961924" fill="white"/>
<rect x="-223" y="650.5381" width="306" height="54.961924" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-218 668.019)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="116" y="15">GetRepo</tspan>
</text>
</g>
<g id="Graphic_30">
<rect x="-12.734375" y="1088.724" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-7.734375 1101.724)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="102.52637" y="15">contentserver-repo-current.json</tspan>
</text>
</g>
<g id="Graphic_31">
<rect x="-69" y="912.816" width="554" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-64 936.316)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="172.144" y="16">Var Directory (aka History)</tspan>
</text>
</g>
<g id="Graphic_34">
<rect x="546" y="912.7753" width="341" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(551 936.2753)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="83.124" y="16">Logfile (JSON or TXT)</tspan>
</text>
</g>
<g id="Graphic_37">
<rect x="-262" y="275" width="384" height="67" fill="white"/>
<rect x="-262" y="275" width="384" height="67" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-257 298.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="173.824" y="16">API</tspan>
</text>
</g>
<g id="Graphic_39">
<rect x="670" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(675 657.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Graphic_40">
<rect x="495" y="623.10984" width="156" height="87.88546" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(500 657.0526)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="65" y="15"></tspan>
</text>
</g>
<g id="Graphic_41">
<rect x="-12.734375" y="1010.3673" width="441.46875" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-7.734375 1023.3673)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="97.70237" y="15">contentserver-repo-[Timestamp].json</tspan>
</text>
</g>
<g id="Graphic_45">
<rect x="569" y="1000.2753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(574 1012.2753)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_44">
<rect x="569" y="1059.7753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(574 1071.7753)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_46">
<rect x="569" y="1119.2753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(574 1131.2753)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Graphic_47">
<rect x="569" y="1178.7753" width="301" height="46" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(574 1190.7753)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="111.19531" y="17">Log entry</tspan>
</text>
</g>
<g id="Line_48">
<line x1="413" y1="120.5" x2="413" y2="67.4" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_49">
<line x1="-469.47585" y1="537.66146" x2="-237.4888" y2="419.19996" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_50">
<line x1="-469.47585" y1="537.66146" x2="-238.52136" y2="480.43586" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_51">
<line x1="-469.47585" y1="537.66146" x2="-236.89965" y2="539.3806" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_53">
<line x1="-464.51305" y1="540.5356" x2="-238.39468" y2="605.75816" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_52">
<line x1="-469.47585" y1="537.66146" x2="-237.1631" y2="672.2017" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_56">
<line x1="626.31374" y1="815" x2="685.49386" y2="901.1427" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_57">
<line x1="268.92984" y1="815" x2="229.43732" y2="900.1143" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_58">
<rect x="-548" y="500.94756" width="119.31591" height="84" fill="white"/>
<rect x="-548" y="500.94756" width="119.31591" height="84" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(-543 532.94756)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="34.065957" y="15">Client</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

260
docs/assets/Update-Flow.svg Normal file
View File

@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="314 98 1205 2185" width="1205" height="2185">
<defs>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 5 3 0 0 0 2 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="951.9958" descent="-212.99744" font-weight="400">
<font-face-src>
<font-face-name name="HelveticaNeue"/>
</font-face-src>
</font-face>
<marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" stroke-linejoin="miter" stroke-miterlimit="10" viewBox="-1 -3 7 6" markerWidth="7" markerHeight="6" color="black">
<g>
<path d="M 4.8 0 L 0 -1.8 L 0 1.8 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
</g>
</marker>
<font-face font-family="Futura" font-size="16" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
<font-face font-family="Helvetica Neue" font-size="16" panose-1="2 0 8 3 0 0 0 9 0 4" units-per-em="1000" underline-position="-100" underline-thickness="50" slope="0" x-height="517" cap-height="714" ascent="975.0061" descent="-216.99524" font-weight="700">
<font-face-src>
<font-face-name name="HelveticaNeue-Bold"/>
</font-face-src>
</font-face>
<font-face font-family="Futura" font-size="80" panose-1="2 11 6 2 2 2 4 2 3 3" units-per-em="1000" underline-position="-97.65625" underline-thickness="78.125" slope="0" x-height="482.4219" cap-height="761.2305" ascent="1038.5742" descent="-259.76562" font-weight="500">
<font-face-src>
<font-face-name name="Futura-Medium"/>
</font-face-src>
</font-face>
</defs>
<metadata> Produced by OmniGraffle 7.10.2
<dc:date>2019-05-29 10:21:17 +0000</dc:date>
</metadata>
<g id="Update-Flow" stroke-opacity="1" fill="none" stroke="none" stroke-dasharray="none" fill-opacity="1">
<title>Update-Flow</title>
<rect fill="white" x="314" y="98" width="1205" height="2185"/>
<g id="Update-Flow: Layer 1">
<title>Layer 1</title>
<g id="Graphic_4">
<rect x="526" y="282" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(531 303.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="89.328" y="15">*Repo.Update()</tspan>
</text>
</g>
<g id="Graphic_5">
<rect x="526" y="425" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(531 446.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="80.144" y="15">*Repo.tryUpdate()</tspan>
</text>
</g>
<g id="Graphic_7">
<rect x="315" y="587" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(320 608.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="23.344" y="15">if updateErr != errUpdateRejected</tspan>
</text>
</g>
<g id="Line_8">
<line x1="632.66975" y1="489" x2="516.5623" y2="578.1441" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_10">
<rect x="545.27846" y="518.599" width="56" height="32" fill="white"/>
<text transform="translate(550.27846 523.599)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">failure</tspan>
</text>
</g>
<g id="Graphic_12">
<rect x="315" y="768" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(320 789.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="43.848" y="15">*Repo.tryToRestoreCurrent()</tspan>
</text>
</g>
<g id="Line_13">
<line x1="464" y1="651" x2="464" y2="754.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_15">
<line x1="675" y1="346" x2="675" y2="411.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_16">
<rect x="1045" y="768" width="473" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1050 789.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="8.42" y="15">*Repo.updateInProgressChan &lt;- make(chan updateResponse)</tspan>
</text>
</g>
<g id="Graphic_18">
<rect x="736" y="768" width="222" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(741 789.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="15.032" y="15">return errUpdateRejected</tspan>
</text>
</g>
<g id="Graphic_19">
<rect x="522.1144" y="518.599" width="102.32812" height="32" fill="white"/>
<text transform="translate(527.1144 523.599)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">update error</tspan>
</text>
</g>
<g id="Line_20">
<line x1="691.2974" y1="489" x2="824.9201" y2="755.4686" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_21">
<line x1="732.4672" y1="489" x2="1212.8041" y2="760.6497" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_22">
<rect x="716.2705" y="603.8538" width="81.28906" height="32" fill="white"/>
<text transform="translate(721.2705 608.8538)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="0" y="17">queue full</tspan>
</text>
</g>
<g id="Graphic_23">
<rect x="924.7738" y="608.0878" width="93.11719" height="32" fill="white"/>
<text transform="translate(929.7738 613.0878)" fill="black">
<tspan font-family="Futura" font-size="16" font-weight="500" fill="black" x="2.46875" y="17">queue free </tspan>
</text>
</g>
<g id="Graphic_24">
<rect x="315" y="964" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(320 985.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="59.832" y="15">return updateResponse</tspan>
</text>
</g>
<g id="Line_25">
<line x1="464" y1="832" x2="464" y2="950.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_28">
<rect x="736" y="883" width="782" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(741 904.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="299.496" y="16">*Repo.updateRoutine()</tspan>
</text>
</g>
<g id="Graphic_29">
<rect x="736" y="946" width="782" height="1072" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_31">
<rect x="767" y="982" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(772 1003.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="13.305457" y="15">resChan &lt;- *Repo.updateInProgressChannel:</tspan>
</text>
</g>
<g id="Graphic_32">
<rect x="816" y="1069" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(821 1090.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="121.24146" y="15">*Repo.update()</tspan>
</text>
</g>
<g id="Graphic_33">
<rect x="878.4145" y="1156" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(883.4145 1177.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="134.88146" y="15">*Repo.get()</tspan>
</text>
</g>
<g id="Graphic_34">
<rect x="949" y="1243" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(954 1264.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="67.75346" y="15">*Repo.loadNodesFromJSON()</tspan>
</text>
</g>
<g id="Graphic_35">
<rect x="1013" y="1330" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1018 1351.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="107.47346" y="15">*Repo.loadNodes()</tspan>
</text>
</g>
<g id="Line_36">
<line x1="1237.837" y1="832" x2="1181.0111" y2="874.2976" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
</g>
<g id="Graphic_37">
<rect x="1056" y="1417" width="409.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1061 1420.828)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="15">for dimension, newNode := range nodes {</tspan>
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="33.448"> *Repo.updateDimension(dimension, newNode)</tspan>
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="0" y="51.895996">}</tspan>
</text>
</g>
<g id="Graphic_38">
<rect x="1106" y="1508" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1111 1529.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="700" fill="black" x="47.369457" y="16">*Repo.dimensionUpdateRoutine()</tspan>
</text>
</g>
<g id="Graphic_39">
<rect x="1106" y="1571" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1111 1592.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="79.44946" y="15">*Repo._updateDimension()</tspan>
</text>
</g>
<g id="Graphic_40">
<rect x="1106" y="1634" width="359.1709" height="343" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_41">
<rect x="1127" y="1648" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1132 1669.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="60.152" y="15">newNode.WireParents()</tspan>
</text>
</g>
<g id="Graphic_42">
<rect x="1127" y="1731.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1132 1753)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="90.072" y="15">buildDirectory()</tspan>
</text>
</g>
<g id="Graphic_43">
<rect x="1127" y="1815" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1132 1836.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="99.872" y="15">wireAliases()</tspan>
</text>
</g>
<g id="Graphic_44">
<rect x="1127" y="1898.5" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(1132 1920)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="20.8" y="15">dimensionUpdateDoneChan &lt;- err</tspan>
</text>
</g>
<g id="Graphic_45">
<rect x="736" y="2078" width="359.1709" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(741 2099.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="71.76146" y="15">*Repo.history.add(jsonBytes)</tspan>
</text>
</g>
<g id="Line_46">
<line x1="946.0763" y1="2019" x2="930.6539" y2="2064.7752" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="8.0,8.0" stroke-width="2"/>
</g>
<g id="Graphic_48">
<rect x="766.5855" y="2219" width="298" height="63" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<text transform="translate(771.5855 2240.5)" fill="black">
<tspan font-family="Helvetica Neue" font-size="16" font-weight="400" fill="black" x="32.208" y="15">resultChan &lt;- updateResponse</tspan>
</text>
</g>
<g id="Line_47">
<line x1="915.5855" y1="2142" x2="915.5855" y2="2205.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_49">
<line x1="1018.9012" y1="1133" x2="1027.1646" y2="1144.5183" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_50">
<line x1="1084.3681" y1="1220" x2="1094.0898" y2="1231.9824" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_51">
<line x1="1152.4935" y1="1307" x2="1161.0333" y2="1318.6088" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_52">
<line x1="1217.9878" y1="1394" x2="1227.2391" y2="1405.8363" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_53">
<line x1="1276" y1="1712" x2="1276" y2="1717.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_54">
<line x1="1276" y1="1795.5" x2="1276" y2="1801.1" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Line_55">
<line x1="1276" y1="1879" x2="1276" y2="1884.6" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
<g id="Graphic_57">
<text transform="translate(411 103)" fill="black">
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" x="0" y="83">Contentserver</tspan>
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" y="83">:</tspan>
<tspan font-family="Futura" font-size="80" font-weight="500" fill="black" y="83"> Update Flow</tspan>
</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

136
go.mod Normal file
View File

@ -0,0 +1,136 @@
module github.com/foomo/contentserver
go 1.24.3
require (
github.com/foomo/keel v0.20.0
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.22.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.40.0
golang.org/x/sync v0.14.0
)
require (
cloud.google.com/go v0.121.1 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/firestore v1.18.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/avast/retry-go/v4 v4.6.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fbiville/markdown-table-formatter v0.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/foomo/gostandards v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/consul/api v1.32.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.42.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/sagikazarmark/crypt v0.28.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.4 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/viper/remote v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect
go.etcd.io/etcd/client/v2 v2.305.21 // indirect
go.etcd.io/etcd/client/v3 v3.6.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.233.0 // indirect
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

731
go.sum Normal file
View File

@ -0,0 +1,731 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute v1.37.0 h1:XxtZlXYkZXub3LNaLu90TTemcFqIU1yZ4E4q9VlR39A=
cloud.google.com/go/compute v1.37.0/go.mod h1:AsK4VqrSyXBo4SMbRtfAO1VfaMjUEjEwv1UB/AwVp5Q=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNYvwXQMMMIKNZop2SSho=
github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/foomo/gostandards v0.2.0 h1:Ryd7TI9yV3Xk5B84DcUDB7KcL3LzQ8NS+TVOrFxTYfA=
github.com/foomo/gostandards v0.2.0/go.mod h1:XQx7Ur6vyvxaIe2cQvAthuhPYDe+d2soibqVcXDXOh4=
github.com/foomo/keel v0.19.0 h1:8uIinFat9Jj72zyWx6c+30f2o0EdXZ350s/caEC37P8=
github.com/foomo/keel v0.19.0/go.mod h1:eyO1lVDIvuIOFjWdIx5MqnWmk0E0FZWZwFhtLiVkTio=
github.com/foomo/keel v0.20.0 h1:kgVPKIdls2hzbuEmD2BTKeCGyV+H38d4Z4Z9ihNKOy0=
github.com/foomo/keel v0.20.0/go.mod h1:AefbM40PS2EJpwZn4EXtQbzbWDZlbYH0N2DB/zQ0YE8=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8=
github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE=
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8=
github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc=
github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E=
github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78=
github.com/sagikazarmark/crypt v0.28.0 h1:g5V74hutj/d3fn5Ga3/3GxUjg1k9H0NfSDjDUcBNpIs=
github.com/sagikazarmark/crypt v0.28.0/go.mod h1:stOy168PraSkc5DJisfihVxPnsXUAVIcIzy/MQh49DA=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM=
github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper/remote v1.20.1 h1:0qVzx4wHqc62HOJDCc/7tcvjLmHjUf4KFQE3RBXfC3k=
github.com/spf13/viper/remote v1.20.1/go.mod h1:Q1UYWvOAkwFm9ntDssWgf1L07rMj1cZ5BerO2gBa6zg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU=
github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c=
go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM=
go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw=
go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A=
go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k=
go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA=
go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI=
go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E=
go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA=
go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8=
go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg=
go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw=
go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk=
go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/host v0.57.0 h1:1gfzOyXEuCrrwCXF81LO3DQ4rll6YBKfAQHPl+03mik=
go.opentelemetry.io/contrib/instrumentation/host v0.57.0/go.mod h1:pHBt+1Rhz99VBX7AQVgwcKPf611zgD6pQy7VwBNMFmE=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0 h1:LD6TMRg2hfNzkMD36Pq0jeYBcSP9W0aJt41Zmje43Ig=
go.opentelemetry.io/contrib/instrumentation/host v0.60.0/go.mod h1:GN4xnih1u2OQeRs8rNJ13XR8XsTqFopc57e/3Kf0h6c=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE=
go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU=
google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o=
google.golang.org/api v0.233.0 h1:iGZfjXAJiUFSSaekVB7LzXl6tRfEKhUN7FkZN++07tI=
google.golang.org/api v0.233.0/go.mod h1:TCIVLLlcwunlMpZIhIp7Ltk77W+vUSdUKAAIlbxY44c=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9 h1:0DnDgelxbooHLt0nyiPeCP0zrH/RL+UG558i1oNU1xE=
google.golang.org/genproto v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:IuQRZAKkz+Mhos3ZZ0+hcGaTmLuuTuGw344uzwztGl8=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,79 +0,0 @@
package log
import (
"fmt"
"strings"
"time"
)
// Level logging level enum
type Level int
const (
// LevelError an error - as bad as it gets
LevelError Level = 0
// LevelRecord put this to the logs in any case
LevelRecord Level = 1
// LevelWarning not that bad
LevelWarning Level = 2
// LevelNotice almost on debug level
LevelNotice Level = 3
// LevelDebug we are debugging
LevelDebug Level = 4
)
// SelectedLevel selected log level
var SelectedLevel = LevelDebug
var prefices = map[Level]string{
LevelRecord: "record : ",
LevelError: "error : ",
LevelWarning: "warning : ",
LevelNotice: "notice : ",
LevelDebug: "debug : ",
}
func log(msg string, level Level) string {
if level <= SelectedLevel {
prefix := time.Now().Format(time.RFC3339Nano) + " " + prefices[level]
lines := strings.Split(msg, "\n")
for i := 0; i < len(lines); i++ {
fmt.Println(prefix + lines[i])
}
}
return msg
}
func logThings(msgs []interface{}, level Level) string {
r := ""
for _, msg := range msgs {
r += "\n" + fmt.Sprint(msg)
}
r = strings.Trim(r, "\n")
return log(r, level)
}
// Debug write debug messages to the log
func Debug(msgs ...interface{}) string {
return logThings(msgs, LevelDebug)
}
// Notice write notice messages to the log
func Notice(msgs ...interface{}) string {
return logThings(msgs, LevelNotice)
}
// Warning write warning messages to the log
func Warning(msgs ...interface{}) string {
return logThings(msgs, LevelWarning)
}
// Record write record messages to the log
func Record(msgs ...interface{}) string {
return logThings(msgs, LevelRecord)
}
// Error write error messages to the log
func Error(msgs ...interface{}) string {
return logThings(msgs, LevelError)
}

9
main.go Normal file
View File

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

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 = ""
)
for {
// let us read with 1 byte steps on conn until we find "{"
_, readErr := conn.Read(headerBuffer[0:])
if errors.Is(readErr, io.EOF) {
// client closed the connection
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
return
} else if readErr != nil {
h.l.Error("failed to read from connection", zap.Error(readErr))
return
}
// read next byte
current := headerBuffer[0:]
if string(current) == "{" {
// json has started
handler, jsonLength, headerErr := h.extractHandlerAndJSONLentgh(header)
// reset header
header = ""
if headerErr != nil {
h.l.Error("invalid request could not read header", zap.Error(headerErr))
encodedErr, encodingErr := h.encodeReply(responses.NewError(4, "invalid header "+headerErr.Error()))
if encodingErr == nil {
h.writeResponse(conn, encodedErr)
} else {
h.l.Error("could not respond to invalid request", zap.Error(encodingErr))
}
return
}
h.l.Debug("found json", zap.Int("length", jsonLength))
if jsonLength > 0 {
var (
// let us try to read some json
jsonBytes = make([]byte, jsonLength)
jsonLengthCurrent = 1
readRound = 0
)
// that is "{"
jsonBytes[0] = 123
for jsonLengthCurrent < jsonLength {
readRound++
readLength, jsonReadErr := conn.Read(jsonBytes[jsonLengthCurrent:jsonLength])
if jsonReadErr != nil {
// @fixme we need to force a read timeout (SetReadDeadline?), if expected jsonLength is lower than really sent bytes (e.g. if client implements protocol wrong)
// @todo should we check for io.EOF here
h.l.Error("could not read json - giving up with this client connection", zap.Error(jsonReadErr))
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
return
}
jsonLengthCurrent += readLength
h.l.Debug("read cycle status",
zap.Int("jsonLengthCurrent", jsonLengthCurrent),
zap.Int("jsonLength", jsonLength),
zap.Int("readRound", readRound),
)
}
h.l.Debug("read json", zap.Int("length", len(jsonBytes)))
h.writeResponse(conn, h.execute(handler, jsonBytes))
// note: connection remains open
continue
}
h.l.Error("can not read empty json")
metrics.NumSocketsGauge.WithLabelValues(conn.RemoteAddr().String()).Dec()
return
}
// adding to header byte by byte
header += string(headerBuffer[0:])
}
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (h *Socket) extractHandlerAndJSONLentgh(header string) (route Route, jsonLength int, err error) {
headerParts := strings.Split(header, ":")
if len(headerParts) != 2 {
return "", 0, errors.New("invalid header")
}
jsonLength, err = strconv.Atoi(headerParts[1])
if err != nil {
err = fmt.Errorf("could not parse length in header: %q", header)
}
return Route(headerParts[0]), jsonLength, err
}
func (h *Socket) execute(route Route, jsonBytes []byte) (reply []byte) {
h.l.Debug("incoming json buffer", zap.Int("length", len(jsonBytes)))
if route == RouteGetRepo {
var (
b bytes.Buffer
)
h.repo.WriteRepoBytes(&b)
return b.Bytes()
}
reply, handlingError := h.handleRequest(h.repo, route, jsonBytes, sourceSocketServer)
if handlingError != nil {
h.l.Error("socketServer.execute failed", zap.Error(handlingError))
}
return reply
}
func (h *Socket) writeResponse(conn net.Conn, reply []byte) {
headerBytes := []byte(strconv.Itoa(len(reply)))
reply = append(headerBytes, reply...)
h.l.Debug("replying", zap.String("reply", string(reply)))
n, writeError := conn.Write(reply)
if writeError != nil {
h.l.Error("socketServer.writeResponse: could not write reply", zap.Error(writeError))
return
}
if n < len(reply) {
h.l.Error("socketServer.writeResponse: write too short",
zap.Int("got", n),
zap.Int("expected", len(reply)),
)
return
}
h.l.Debug("replied. waiting for next request on open connection")
}
func (h *Socket) handleRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) ([]byte, error) {
start := time.Now()
reply, err := h.executeRequest(r, route, jsonBytes, source)
result := "success"
if err != nil {
result = "error"
}
metrics.ServiceRequestCounter.WithLabelValues(string(route), result, source).Inc()
metrics.ServiceRequestDuration.WithLabelValues(string(route), result, source).Observe(time.Since(start).Seconds())
return reply, err
}
func (h *Socket) executeRequest(r *repo.Repo, route Route, jsonBytes []byte, source string) (replyBytes []byte, err error) {
var (
reply interface{}
apiErr error
jsonErr error
processIfJSONIsOk = func(err error, processingFunc func()) {
if err != nil {
jsonErr = err
return
}
processingFunc()
}
)
metrics.ContentRequestCounter.WithLabelValues(source).Inc()
// handle and process
switch route {
// case RouteGetRepo: // This case is handled prior to handleRequest being called.
// since the resulting bytes are written directly in to the http.ResponseWriter / net.Connection
case RouteGetURIs:
getURIRequest := &requests.URIs{}
processIfJSONIsOk(json.Unmarshal(jsonBytes, &getURIRequest), func() {
reply = r.GetURIs(getURIRequest.Dimension, getURIRequest.IDs)
})
case RouteGetContent:
contentRequest := &requests.Content{}
processIfJSONIsOk(json.Unmarshal(jsonBytes, &contentRequest), func() {
reply, apiErr = r.GetContent(contentRequest)
})
case RouteGetNodes:
nodesRequest := &requests.Nodes{}
processIfJSONIsOk(json.Unmarshal(jsonBytes, &nodesRequest), func() {
reply = r.GetNodes(nodesRequest)
})
case RouteUpdate:
updateRequest := &requests.Update{}
processIfJSONIsOk(json.Unmarshal(jsonBytes, &updateRequest), func() {
reply = r.Update()
})
default:
reply = responses.NewError(1, "unknown handler: "+string(route))
}
// error handling
if jsonErr != nil {
h.l.Error("could not read incoming json", zap.Error(jsonErr))
reply = responses.NewError(2, "could not read incoming json "+jsonErr.Error())
} else if apiErr != nil {
h.l.Error("an API error occurred", zap.Error(apiErr))
reply = responses.NewError(3, "internal error "+apiErr.Error())
}
return h.encodeReply(reply)
}
// encodeReply takes an interface and encodes it as JSON
// it returns the resulting JSON and a marshalling error
func (h *Socket) encodeReply(reply interface{}) (replyBytes []byte, err error) {
replyBytes, err = json.Marshal(map[string]interface{}{
"reply": reply,
})
if err != nil {
h.l.Error("could not encode reply", zap.Error(err))
}
return
}

100
pkg/metrics/metrics.go Normal file
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
}

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

@ -0,0 +1,180 @@
package repo
import (
"bytes"
"fmt"
"io"
"os"
"path"
"sort"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
)
const (
HistoryRepoJSONPrefix = "contentserver-repo-"
HistoryRepoJSONSuffix = ".json"
)
type (
History struct {
l *zap.Logger
historyDir string
historyLimit int
currentMutext sync.RWMutex
}
HistoryOption func(*History)
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func HistoryWithHistoryLimit(v int) HistoryOption {
return func(o *History) {
o.historyLimit = v
}
}
func HistoryWithHistoryDir(v string) HistoryOption {
return func(o *History) {
o.historyDir = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewHistory(l *zap.Logger, opts ...HistoryOption) *History {
inst := &History{
l: l,
historyDir: "/var/lib/contentserver",
historyLimit: 2,
}
for _, opt := range opts {
opt(inst)
}
return inst
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (h *History) Add(jsonBytes []byte) error {
backupFilename := path.Join(h.historyDir, HistoryRepoJSONPrefix+time.Now().Format(time.RFC3339Nano)+HistoryRepoJSONSuffix)
currentFilename := h.GetCurrentFilename()
if err := os.MkdirAll(path.Dir(backupFilename), 0700); err != nil {
return errors.Wrap(err, "failed to create history dir")
}
if err := os.WriteFile(backupFilename, jsonBytes, 0600); err != nil {
return errors.Wrap(err, "failed to write backup history file")
}
h.l.Debug("writing files",
zap.String("backup", backupFilename),
zap.String("current", currentFilename),
)
// current filename
h.currentMutext.Lock()
defer h.currentMutext.Unlock()
if err := os.WriteFile(currentFilename, jsonBytes, 0600); err != nil {
return errors.Wrap(err, "failed to write current history")
}
if err := h.cleanup(); err != nil {
return errors.Wrap(err, "failed to clean up history")
}
return nil
}
func (h *History) GetCurrentFilename() string {
return path.Join(h.historyDir, HistoryRepoJSONPrefix+"current"+HistoryRepoJSONSuffix)
}
func (h *History) GetCurrent(buf *bytes.Buffer) error {
h.currentMutext.RLock()
defer h.currentMutext.RUnlock()
f, err := os.Open(h.GetCurrentFilename())
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(buf, f)
return err
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (h *History) getHistory() (files []string, err error) {
fileInfos, err := os.ReadDir(h.historyDir)
if err != nil {
return
}
currentName := h.GetCurrentFilename()
for _, f := range fileInfos {
if !f.IsDir() {
filename := f.Name()
if filename != currentName && (strings.HasPrefix(filename, HistoryRepoJSONPrefix) && strings.HasSuffix(filename, HistoryRepoJSONSuffix)) {
files = append(files, path.Join(h.historyDir, filename))
}
}
}
sort.Sort(sort.Reverse(sort.StringSlice(files)))
return
}
func (h *History) cleanup() error {
files, err := h.getFilesForCleanup(h.historyLimit)
if err != nil {
return err
}
for _, f := range files {
h.l.Debug("removing outdated backup", zap.String("file", f))
err := os.Remove(f)
if err != nil {
return fmt.Errorf("could not remove file %s : %s", f, err.Error())
}
}
return nil
}
func (h *History) getFilesForCleanup(historyVersions int) (files []string, err error) {
contentFiles, err := h.getHistory()
if err != nil {
return nil, errors.New("could not generate file cleanup list: " + err.Error())
}
// fmt.Println("contentFiles:")
// for _, f := range contentFiles {
// fmt.Println(f)
// }
// -1 to remove the current backup file from the number of items
// so that only files with a timestamp are compared
if len(contentFiles)-1 > historyVersions {
for i := historyVersions + 1; i < len(contentFiles); i++ {
// ignore current repository file to fall back on
if contentFiles[i] == h.GetCurrentFilename() {
continue
}
files = append(files, contentFiles[i])
}
}
return files, nil
}

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

@ -0,0 +1,72 @@
package repo
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
func TestHistoryCurrent(t *testing.T) {
var (
h = testHistory(t)
test = []byte("test")
b bytes.Buffer
)
err := h.Add(test)
require.NoError(t, err)
err = h.GetCurrent(&b)
require.NoError(t, err)
if !bytes.Equal(b.Bytes(), test) {
t.Fatalf("expected %q, got %q", string(test), b.String())
}
}
func TestHistoryCleanup(t *testing.T) {
h := testHistory(t)
for i := 0; i < 50; i++ {
err := h.Add([]byte(fmt.Sprint(i)))
require.NoError(t, err)
time.Sleep(time.Millisecond * 5)
}
err := h.cleanup()
require.NoError(t, err)
files, err := h.getHistory()
require.NoError(t, err)
// -1 for ignoring the current content backup file
if len(files)-1 != 2 {
t.Fatal("history too long", len(files), "instead of", 2)
}
}
func TestHistoryOrder(t *testing.T) {
h := testHistory(t)
h.historyDir = "testdata/order"
files, err := h.getHistory()
require.NoError(t, err)
assert.Equal(t, "testdata/order/contentserver-repo-current.json", files[0])
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-23.json", files[1])
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-22.json", files[2])
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-21.json", files[3])
}
func TestGetFilesForCleanup(t *testing.T) {
h := testHistory(t)
h.historyDir = "testdata/order"
files, err := h.getFilesForCleanup(2)
require.NoError(t, err)
assert.Equal(t, "testdata/order/contentserver-repo-2017-10-21.json", files[0])
}
func testHistory(t *testing.T) *History {
t.Helper()
l := zaptest.NewLogger(t)
return NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(t.TempDir()))
}

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

@ -0,0 +1,394 @@
package repo
import (
"bytes"
"context"
"io"
"net/http"
"time"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/pkg/metrics"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"go.uber.org/multierr"
"go.uber.org/zap"
)
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
ErrUpdateRejected = errors.New("update rejected: queue full")
)
type updateResponse struct {
repoRuntime int64
err error
}
func (r *Repo) PollRoutine(ctx context.Context) error {
l := r.l.Named("routine.poll")
ticker := time.NewTicker(r.pollInterval)
for {
select {
case <-ctx.Done():
l.Debug("routine canceled")
return nil
case <-ticker.C:
chanReponse := make(chan updateResponse)
r.updateInProgressChannel <- chanReponse
response := <-chanReponse
if response.err == nil {
l.Info("update success", zap.String("revision", r.pollVersion))
} else {
l.Error("update failed", zap.Error(response.err))
}
}
}
}
func (r *Repo) UpdateRoutine(ctx context.Context) error {
l := r.l.Named("routine.update")
for {
select {
case <-ctx.Done():
l.Debug("routine canceled")
return nil
case resChan := <-r.updateInProgressChannel:
start := time.Now()
l := l.With(zap.String("run_id", uuid.New().String()))
l.Info("update started")
repoRuntime, err := r.update(context.WithoutCancel(ctx))
if err != nil {
l.Error("update failed", zap.Error(err))
metrics.UpdatesFailedCounter.WithLabelValues().Inc()
} else {
if !r.Loaded() {
r.loaded.Store(true)
l.Info("initial update success")
if r.onLoaded != nil {
r.onLoaded()
}
} else {
l.Info("update success")
}
metrics.UpdatesCompletedCounter.WithLabelValues().Inc()
}
resChan <- updateResponse{
repoRuntime: repoRuntime,
err: err,
}
metrics.UpdateDuration.WithLabelValues().Observe(time.Since(start).Seconds())
}
}
}
func (r *Repo) DimensionUpdateRoutine(ctx context.Context) error {
l := r.l.Named("routine.dimensionUpdate")
for {
select {
case <-ctx.Done():
l.Debug("routine canceled")
return nil
case newDimension := <-r.dimensionUpdateChannel:
l.Debug("received a new dimension", zap.String("dimension", newDimension.Dimension))
err := r._updateDimension(newDimension.Dimension, newDimension.Node)
l.Info("received result")
if err != nil {
l.Debug("update failed", zap.Error(err))
}
r.dimensionUpdateDoneChannel <- err
}
}
}
func (r *Repo) updateDimension(dimension string, node *content.RepoNode) error {
r.l.Debug("trying to push dimension into update channel", zap.String("dimension", dimension), zap.String("nodeName", node.Name))
r.dimensionUpdateChannel <- &RepoDimension{
Dimension: dimension,
Node: node,
}
r.l.Debug("waiting for done signal")
return <-r.dimensionUpdateDoneChannel
}
// do not call directly, but only through channel
func (r *Repo) _updateDimension(dimension string, newNode *content.RepoNode) error {
newNode.WireParents()
var (
newDirectory = make(map[string]*content.RepoNode)
newURIDirectory = make(map[string]*content.RepoNode)
err = buildDirectory(newNode, newDirectory, newURIDirectory)
)
if err != nil {
return errors.New("update dimension \"" + dimension + "\" failed when building its directory:: " + err.Error())
}
err = wireAliases(newDirectory)
if err != nil {
return err
}
// ---------------------------------------------
// copy old datastructure to prevent concurrent map access
// collect other dimension in the Directory
newRepoDirectory := map[string]*Dimension{}
for d, D := range r.Directory() {
if d != dimension {
newRepoDirectory[d] = D
}
}
// add the new dimension
newRepoDirectory[dimension] = &Dimension{
Node: newNode,
Directory: newDirectory,
URIDirectory: newURIDirectory,
}
r.SetDirectory(newRepoDirectory)
// ---------------------------------------------
// @TODO: why not update only the dimension that has changed instead?
// repo.Directory[dimension] = &Dimension{
// Node: newNode,
// Directory: newDirectory,
// URIDirectory: newURIDirectory,
// }
// ---------------------------------------------
return nil
}
func buildDirectory(dirNode *content.RepoNode, directory map[string]*content.RepoNode, uRIDirectory map[string]*content.RepoNode) error {
existingNode, ok := directory[dirNode.ID]
if ok {
return errors.New("duplicate node with id:" + existingNode.ID)
}
directory[dirNode.ID] = dirNode
// todo handle duplicate uris
if _, thereIsAnExistingURINode := uRIDirectory[dirNode.URI]; thereIsAnExistingURINode {
return errors.New("duplicate uri: " + dirNode.URI + " (bad node id: " + dirNode.ID + ")")
}
uRIDirectory[dirNode.URI] = dirNode
for _, childNode := range dirNode.Nodes {
err := buildDirectory(childNode, directory, uRIDirectory)
if err != nil {
return err
}
}
return nil
}
func wireAliases(directory map[string]*content.RepoNode) error {
for _, repoNode := range directory {
if len(repoNode.LinkID) > 0 {
if destinationNode, ok := directory[repoNode.LinkID]; ok {
repoNode.URI = destinationNode.URI
} else {
return errors.New("that link id points nowhere " + repoNode.LinkID + " from " + repoNode.ID)
}
}
}
return nil
}
func (r *Repo) loadNodesFromJSON() (nodes map[string]*content.RepoNode, err error) {
nodes = make(map[string]*content.RepoNode)
err = json.Unmarshal(r.JSONBufferBytes(), &nodes)
if err != nil {
r.l.Error("Failed to deserialize nodes", zap.Error(err))
return nil, errors.New("failed to deserialize nodes")
}
return nodes, nil
}
func (r *Repo) tryToRestoreCurrent() error {
buffer := &bytes.Buffer{}
err := r.history.GetCurrent(buffer)
if err != nil {
return err
}
r.SetJSONBuffer(buffer)
return r.loadJSONBytes()
}
func (r *Repo) get(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return errors.Wrap(err, "failed to create get repo request")
}
response, err := r.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "failed to get repo")
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return errors.Errorf("bad response code from repository %q want %q", response.Status, http.StatusOK)
}
// Log.Info(ansi.Red + "RESETTING BUFFER" + ansi.Reset)
buffer := &bytes.Buffer{}
// Log.Info(ansi.Green + "LOADING DATA INTO BUFFER" + ansi.Reset)
_, err = io.Copy(buffer, response.Body)
if err != nil {
return errors.Wrap(err, "failed to copy IO stream")
}
r.SetJSONBuffer(buffer)
return nil
}
func (r *Repo) update(ctx context.Context) (repoRuntime int64, err error) {
startTimeRepo := time.Now().UnixNano()
repoURL := r.url
if r.poll {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.url, nil)
if err != nil {
return repoRuntime, err
}
resp, err := r.httpClient.Do(req)
if err != nil {
return repoRuntime, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return repoRuntime, errors.New("could not poll latest repo download url - non 200 response")
}
responseBytes, err := io.ReadAll(resp.Body)
if err != nil {
return repoRuntime, errors.New("could not poll latest repo download url, could not read body")
}
repoURL = string(responseBytes)
if repoURL == r.pollVersion {
r.l.Info(
"repo is up to date",
zap.String("pollVersion", r.pollVersion),
)
// already up to date
return repoRuntime, nil
}
r.l.Info(
"new repo poll version",
zap.String("pollVersion", r.pollVersion),
)
}
err = r.get(ctx, repoURL)
repoRuntime = time.Now().UnixNano() - startTimeRepo
if err != nil {
// we have no json to load - the repo server did not reply
r.l.Debug("failed to load json", zap.Error(err))
return repoRuntime, err
}
r.l.Debug("loading json", zap.String("server", repoURL), zap.Int("length", len(r.JSONBufferBytes())))
nodes, err := r.loadNodesFromJSON()
if err != nil {
// could not load nodes from json
return repoRuntime, err
}
err = r.loadNodes(nodes)
if err != nil {
// repo failed to load nodes
return repoRuntime, err
}
if r.poll {
r.pollVersion = repoURL
}
// Persist the JSON buffer after successful update
if err := r.history.Add(r.JSONBufferBytes()); err != nil {
r.l.Error("Failed to persist repo after update", zap.Error(err))
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
} else {
r.l.Info("Successfully persisted repo after update")
}
return repoRuntime, nil
}
// limit ressources and allow only one update request at once
func (r *Repo) tryUpdate() (repoRuntime int64, err error) {
c := make(chan updateResponse)
select {
case r.updateInProgressChannel <- c:
r.l.Debug("update request added to queue")
ur := <-c
return ur.repoRuntime, ur.err
default:
r.l.Info("update request accepted, will be processed after the previous update")
return 0, ErrUpdateRejected
}
}
func (r *Repo) loadJSONBytes() error {
nodes, err := r.loadNodesFromJSON()
if err != nil {
data := r.JSONBufferBytes()
if len(data) > 10 {
r.l.Debug("could not parse json",
zap.String("jsonStart", string(data[:10])),
zap.String("jsonStart", string(data[len(data)-10:])),
)
}
return err
}
err = r.loadNodes(nodes)
if err == nil {
errHistory := r.history.Add(r.JSONBufferBytes())
if errHistory != nil {
r.l.Error("Could not add valid JSON to history", zap.Error(errHistory))
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
} else {
r.l.Info("added valid JSON to history")
}
}
return err
}
func (r *Repo) loadNodes(newNodes map[string]*content.RepoNode) error {
var err error
newDimensions := make([]string, 0, len(newNodes))
for dimension, newNode := range newNodes {
newDimensions = append(newDimensions, dimension)
r.l.Debug("loading nodes for dimension", zap.String("dimension", dimension))
errLoad := r.updateDimension(dimension, newNode)
if errLoad != nil {
err = multierr.Append(err, errLoad)
}
}
if err != nil {
return errors.Wrap(err, "failed to update dimension")
}
dimensionIsValid := func(dimension string) bool {
for _, newDimension := range newDimensions {
if dimension == newDimension {
return true
}
}
return false
}
// we need to throw away orphaned dimensions
directory := map[string]*Dimension{}
for dimension, value := range r.Directory() {
if !dimensionIsValid(dimension) {
r.l.Info("removing orphaned dimension", zap.String("dimension", dimension))
continue
}
directory[dimension] = value
}
r.SetDirectory(directory)
return nil
}

View File

@ -1,7 +1,6 @@
package mock
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"path"
@ -9,26 +8,27 @@ import (
"testing"
"time"
"github.com/foomo/contentserver/log"
"github.com/foomo/contentserver/requests"
)
// GetMockData mock data to run a repo
func GetMockData(t testing.TB) (server *httptest.Server, varDir string) {
log.SelectedLevel = log.LevelError
func GetMockData(tb testing.TB) (*httptest.Server, string) {
tb.Helper()
_, filename, _, _ := runtime.Caller(0)
mockDir := path.Dir(filename)
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
time.Sleep(time.Millisecond * 50)
mockFilename := path.Join(mockDir, req.URL.Path[1:])
http.ServeFile(w, req, mockFilename)
}))
varDir, err := ioutil.TempDir("", "content-server-test")
if err != nil {
panic(err)
}
return server, varDir
go func() {
<-tb.Context().Done()
server.Close()
}()
return server, tb.TempDir()
}
// MakeNodesRequest a request to get some nodes
@ -38,7 +38,7 @@ func MakeNodesRequest() *requests.Nodes {
Dimensions: []string{"dimension_foo"},
},
Nodes: map[string]*requests.Node{
"test": &requests.Node{
"test": {
ID: "id-root",
Dimension: "dimension_foo",
MimeTypes: []string{},
@ -67,7 +67,7 @@ func MakeValidContentRequest() *requests.Content {
Groups: []string{},
},
Nodes: map[string]*requests.Node{
"id-root": &requests.Node{
"id-root": {
ID: "id-root",
Dimension: dimensions[0],
MimeTypes: []string{"application/x-node"},
@ -76,5 +76,4 @@ func MakeValidContentRequest() *requests.Content {
},
},
}
}

View File

@ -0,0 +1,51 @@
{
"dimension_foo": {
"id": "id-root",
"name": "root node",
"mimeType": "application\/x-node",
"groups": null,
"URI": "\/",
"hidden": false,
"linkId": null,
"destinationId": null,
"data": {
"foo": "bar"
},
"index": [
"id-a",
"id-b"
],
"nodes": {
"id-a": {
"id": "id-a",
"name": "node a",
"mimeType": "application\/x-node",
"groups": null,
"URI": "\/a",
"hidden": false,
"linkId": null,
"destinationId": null,
"data": {
"baz": 1
},
"index": [],
"nodes": {}
},
"id-b": {
"id": "id-b",
"name": "node b",
"mimeType": "application\/x-node",
"groups": null,
"URI": "\/b",
"hidden": true,
"linkId": null,
"destinationId": null,
"data": {
"b": "b"
},
"index": [],
"nodes": {}
}
}
}
}

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

@ -0,0 +1,524 @@
package repo
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/pkg/metrics"
"github.com/foomo/contentserver/requests"
"github.com/foomo/contentserver/responses"
"github.com/pkg/errors"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
const maxGetURIForNodeRecursionLevel = 1000
// Repo content repository
type (
Repo struct {
l *zap.Logger
url string
poll bool
pollInterval time.Duration
pollVersion string
onLoaded func()
loaded *atomic.Bool
history *History
httpClient *http.Client
dimensionUpdateChannel chan *RepoDimension
dimensionUpdateDoneChannel chan error
updateInProgressChannel chan chan updateResponse
directory map[string]*Dimension
directoryLock sync.RWMutex
jsonBuffer *bytes.Buffer
jsonBufferLock sync.RWMutex
}
Option func(*Repo)
)
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func New(l *zap.Logger, url string, history *History, opts ...Option) *Repo {
inst := &Repo{
l: l.Named("repo"),
url: url,
poll: false,
loaded: &atomic.Bool{},
pollInterval: time.Minute,
history: history,
httpClient: http.DefaultClient,
directory: map[string]*Dimension{},
dimensionUpdateChannel: make(chan *RepoDimension),
dimensionUpdateDoneChannel: make(chan error),
updateInProgressChannel: make(chan chan updateResponse),
}
for _, opt := range opts {
opt(inst)
}
return inst
}
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func WithHTTPClient(v *http.Client) Option {
return func(o *Repo) {
o.httpClient = v
}
}
func WithPoll(v bool) Option {
return func(o *Repo) {
o.poll = v
}
}
func WithPollInterval(v time.Duration) Option {
return func(o *Repo) {
o.pollInterval = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Getter
// ------------------------------------------------------------------------------------------------
func (r *Repo) Loaded() bool {
return r.loaded.Load()
}
func (r *Repo) Directory() map[string]*Dimension {
r.directoryLock.RLock()
defer r.directoryLock.RUnlock()
return r.directory
}
func (r *Repo) SetDirectory(v map[string]*Dimension) {
r.directoryLock.Lock()
defer r.directoryLock.Unlock()
r.directory = v
}
func (r *Repo) JSONBufferBytes() []byte {
r.jsonBufferLock.RLock()
defer r.jsonBufferLock.RUnlock()
return r.jsonBuffer.Bytes()
}
func (r *Repo) SetJSONBuffer(v *bytes.Buffer) {
r.jsonBufferLock.Lock()
defer r.jsonBufferLock.Unlock()
r.jsonBuffer = v
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (r *Repo) OnLoaded(fn func()) {
r.onLoaded = fn
}
// GetURIs get many uris at once
func (r *Repo) GetURIs(dimension string, ids []string) map[string]string {
uris := map[string]string{}
for _, id := range ids {
uris[id] = r.getURI(dimension, id)
}
return uris
}
// GetNodes get nodes
func (r *Repo) GetNodes(nodes *requests.Nodes) map[string]*content.Node {
return r.getNodes(nodes.Nodes, nodes.Env)
}
// GetContent resolves content and fetches nodes in one call. It combines those
// two tasks for performance reasons.
//
// In the first step it uses r.URI to look up content in all given
// r.Env.Dimensions of repo.Directory.
//
// In the second step it collects the requested nodes.
//
// those two steps are independent.
func (r *Repo) GetContent(req *requests.Content) (*content.SiteContent, error) {
// add more input validation
err := r.validateContentRequest(req)
if err != nil {
return nil, errors.Wrap(err, "repo.GetContent invalid request")
}
r.l.Debug("repo.GetContent", zap.String("URI", req.URI))
c := content.NewSiteContent()
resolved, resolvedURI, resolvedDimension, node := r.resolveContent(req.Env.Dimensions, req.URI)
if resolved {
if !node.CanBeAccessedByGroups(req.Env.Groups) {
r.l.Warn("Resolved content cannot be accessed by specified group", zap.String("uri", req.URI))
c.Status = content.StatusForbidden
} else {
r.l.Info("Content resolved", zap.String("uri", req.URI))
c.Status = content.StatusOk
c.Data = node.Data
}
c.MimeType = node.MimeType
c.Dimension = resolvedDimension
c.URI = resolvedURI
c.Item = node.ToItem(req.DataFields)
c.Path = node.GetPath(req.PathDataFields)
// fetch URIs for all dimensions
uris := make(map[string]string)
for dimensionName := range r.Directory() {
uris[dimensionName] = r.getURI(dimensionName, node.ID)
}
c.URIs = uris
} else {
r.l.Info("Content not found", zap.String("URI", req.URI))
c.Status = content.StatusNotFound
c.Dimension = req.Env.Dimensions[0]
r.l.Debug("Failed to resolve, falling back to default dimension",
zap.String("uri", req.URI),
zap.String("default_dimension", req.Env.Dimensions[0]),
)
// r.Env.Dimensions is validated => we can access it
resolvedDimension = req.Env.Dimensions[0]
}
// add navigation trees
for _, node := range req.Nodes {
if node.Dimension == "" {
node.Dimension = resolvedDimension
}
}
c.Nodes = r.getNodes(req.Nodes, req.Env)
return c, nil
}
// GetRepo get the whole repo in all dimensions
func (r *Repo) GetRepo() map[string]*content.RepoNode {
response := make(map[string]*content.RepoNode)
for dimensionName, dimension := range r.Directory() {
response[dimensionName] = dimension.Node
}
return response
}
// WriteRepoBytes get the whole repo in all dimensions
// reads the JSON history file from the Filesystem and copies it directly in to the supplied buffer
// the result is wrapped as service response, e.g: {"reply": <contentData>}
func (r *Repo) WriteRepoBytes(w io.Writer) {
filename := r.history.GetCurrentFilename()
r.history.currentMutext.RLock()
defer r.history.currentMutext.RUnlock()
f, err := os.Open(r.history.GetCurrentFilename())
if err != nil {
r.l.Error("failed to open repo JSON", zap.Error(err), zap.String("history", filename))
return
}
if _, err := w.Write([]byte("{\"reply\":")); err != nil {
r.l.Error("failed to write repo JSON prefix", zap.Error(err))
return
}
if _, err := io.Copy(w, f); err != nil {
r.l.Error("failed to serve repo JSON", zap.Error(err))
return
}
if _, err := w.Write([]byte("}")); err != nil {
r.l.Error("failed to write repo JSON suffix", zap.Error(err))
return
}
}
func (r *Repo) Update() (updateResponse *responses.Update) {
floatSeconds := func(nanoSeconds int64) float64 {
return float64(nanoSeconds) / float64(1000000000)
}
r.l.Info("Update triggered")
// Log.Info(ansi.Yellow + "BUFFER LENGTH BEFORE tryUpdate(): " + strconv.Itoa(len(repo.jsonBuf.Bytes())) + ansi.Reset)
start := time.Now()
updateRepotime, err := r.tryUpdate()
updateResponse = &responses.Update{}
updateResponse.Stats.RepoRuntime = floatSeconds(updateRepotime)
if err != nil {
updateResponse.Success = false
updateResponse.Stats.NumberOfNodes = -1
updateResponse.Stats.NumberOfURIs = -1
// let us try to restore the world from a file
// Log.Info(ansi.Yellow + "BUFFER LENGTH AFTER ERROR: " + strconv.Itoa(len(r.jsonBuf.Bytes())) + ansi.Reset)
// only try to restore if the update failed during processing
if !errors.Is(err, ErrUpdateRejected) {
updateResponse.ErrorMessage = err.Error()
r.l.Error("Failed to update repository", zap.Error(err))
restoreErr := r.tryToRestoreCurrent()
if restoreErr != nil {
r.l.Error("Failed to restore preceding repository version", zap.Error(restoreErr))
} else {
r.l.Info("Successfully restored current repository from local history")
}
}
} else {
updateResponse.Success = true
// persist the currently loaded one
historyErr := r.history.Add(r.JSONBufferBytes())
if historyErr != nil {
r.l.Error("Could not persist current repo in history", zap.Error(historyErr))
metrics.HistoryPersistFailedCounter.WithLabelValues().Inc()
} else {
r.l.Info("Successfully persisted current repo to history")
}
// add some stats
for _, dimension := range r.Directory() {
updateResponse.Stats.NumberOfNodes += len(dimension.Directory)
updateResponse.Stats.NumberOfURIs += len(dimension.URIDirectory)
}
}
updateResponse.Stats.OwnRuntime = floatSeconds(time.Since(start).Nanoseconds()) - updateResponse.Stats.RepoRuntime
return updateResponse
}
func (r *Repo) Start(ctx context.Context) error {
g, gCtx := errgroup.WithContext(ctx)
l := r.l.Named("start")
up := make(chan bool, 1)
g.Go(func() error {
l.Debug("starting update routine")
up <- true
return r.UpdateRoutine(gCtx)
})
l.Debug("waiting for UpdateRoutine")
<-up
g.Go(func() error {
l.Debug("starting dimension update routine")
up <- true
return r.DimensionUpdateRoutine(gCtx)
})
l.Debug("waiting for DimensionUpdateRoutine")
<-up
l.Debug("trying to restore previous repo")
if err := r.tryToRestoreCurrent(); errors.Is(err, os.ErrNotExist) {
l.Info("previous repo content file does not exist")
} else if err != nil {
l.Warn("could not restore previous repo content", zap.Error(err))
} else {
l.Info("restored previous repo")
}
if r.poll {
g.Go(func() error {
l.Debug("starting poll routine")
return r.PollRoutine(gCtx)
})
}
if !r.Loaded() {
l.Debug("trying to update initial state")
if resp := r.Update(); !resp.Success {
l.Error("failed to update initial state",
zap.String("error", resp.ErrorMessage),
zap.Int("num_modes", resp.Stats.NumberOfNodes),
zap.Int("num_uris", resp.Stats.NumberOfURIs),
zap.Float64("own_runtime", resp.Stats.OwnRuntime),
zap.Float64("repo_runtime", resp.Stats.RepoRuntime),
)
}
}
return g.Wait()
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (r *Repo) getNodes(nodeRequests map[string]*requests.Node, env *requests.Env) map[string]*content.Node {
var (
path []*content.Item
nodes = map[string]*content.Node{}
)
for nodeName, nodeRequest := range nodeRequests {
if nodeName == "" || nodeRequest.ID == "" {
r.l.Warn("invalid node request", zap.Error(errors.New("nodeName or nodeRequest.ID empty")))
continue
}
r.l.Debug("adding node", zap.String("name", nodeName), zap.String("requestID", nodeRequest.ID))
groups := env.Groups
if len(nodeRequest.Groups) > 0 {
groups = nodeRequest.Groups
}
dimensionNode, ok := r.Directory()[nodeRequest.Dimension]
nodes[nodeName] = nil
if !ok && nodeRequest.Dimension == "" {
r.l.Debug("Could not get dimension root node", zap.String("dimension", nodeRequest.Dimension))
for _, dimension := range env.Dimensions {
dimensionNode, ok = r.Directory()[dimension]
if ok {
r.l.Debug("Found root node in env.Dimensions", zap.String("dimension", dimension))
break
}
r.l.Debug("Could NOT find root node in env.Dimensions", zap.String("dimension", dimension))
}
}
if !ok {
r.l.Error("could not get dimension root node", zap.String("nodeRequest.Dimension", nodeRequest.Dimension))
continue
}
treeNode, ok := dimensionNode.Directory[nodeRequest.ID]
if !ok {
r.l.Error("Invalid tree node requested",
zap.String("nodeName", nodeName),
zap.String("nodeID", nodeRequest.ID),
)
metrics.InvalidNodeTreeRequests.WithLabelValues().Inc()
continue
}
nodes[nodeName] = r.getNode(treeNode, nodeRequest.Expand, nodeRequest.MimeTypes, path, 0, groups, nodeRequest.DataFields, nodeRequest.ExposeHiddenNodes)
}
return nodes
}
// resolveContent find content in a repository
func (r *Repo) resolveContent(dimensions []string, uri string) (resolved bool, resolvedURI string, resolvedDimension string, repoNode *content.RepoNode) {
parts := strings.Split(uri, content.PathSeparator)
r.l.Debug("repo.ResolveContent", zap.String("URI", uri))
for i := len(parts); i > 0; i-- {
testURI := strings.Join(parts[0:i], content.PathSeparator)
if testURI == "" {
testURI = content.PathSeparator
}
for _, dimension := range dimensions {
if d, ok := r.Directory()[dimension]; ok {
r.l.Debug("Checking node",
zap.String("dimension", dimension),
zap.String("URI", testURI),
)
if repoNode, ok := d.URIDirectory[testURI]; ok {
resolved = true
r.l.Debug("Node found", zap.String("URI", testURI), zap.String("destination", repoNode.DestinationID))
if len(repoNode.DestinationID) > 0 {
if destionationNode, destinationNodeOk := d.Directory[repoNode.DestinationID]; destinationNodeOk {
repoNode = destionationNode
}
}
return resolved, testURI, dimension, repoNode
}
}
}
}
return
}
func (r *Repo) getURIForNode(dimension string, repoNode *content.RepoNode, recursionLevel int64) (uri string) {
if len(repoNode.LinkID) == 0 {
uri = repoNode.URI
return
}
linkedNode, ok := r.Directory()[dimension].Directory[repoNode.LinkID]
if ok {
if recursionLevel > maxGetURIForNodeRecursionLevel {
r.l.Error("maxGetURIForNodeRecursionLevel reached", zap.String("repoNode.ID", repoNode.ID), zap.String("linkID", repoNode.LinkID), zap.String("dimension", dimension))
return ""
}
return r.getURIForNode(dimension, linkedNode, recursionLevel+1)
}
return
}
func (r *Repo) getURI(dimension string, id string) string {
directory, ok := r.Directory()[dimension]
if !ok {
return ""
}
repoNode, ok := directory.Directory[id]
if !ok {
return ""
}
return r.getURIForNode(dimension, repoNode, 0)
}
func (r *Repo) getNode(
repoNode *content.RepoNode,
expanded bool,
mimeTypes []string,
path []*content.Item,
level int,
groups []string,
dataFields []string,
exposeHiddenNodes bool,
) *content.Node {
node := content.NewNode()
node.Item = repoNode.ToItem(dataFields)
r.l.Debug("getNode", zap.String("ID", repoNode.ID))
for _, childID := range repoNode.Index {
childNode := repoNode.Nodes[childID]
if (level == 0 || expanded || !expanded && childNode.InPath(path)) && (!childNode.Hidden || exposeHiddenNodes) && childNode.CanBeAccessedByGroups(groups) && childNode.IsOneOfTheseMimeTypes(mimeTypes) {
node.Nodes[childID] = r.getNode(childNode, expanded, mimeTypes, path, level+1, groups, dataFields, exposeHiddenNodes)
node.Index = append(node.Index, childID)
}
}
return node
}
func (r *Repo) validateContentRequest(req *requests.Content) (err error) {
if req == nil {
return errors.New("request must not be nil")
}
if len(req.URI) == 0 {
return errors.New("request URI must not be empty")
}
if req.Env == nil {
return errors.New("request.Env must not be nil")
}
if len(req.Env.Dimensions) == 0 {
return errors.New("request.Env.Dimensions must not be empty")
}
for _, envDimension := range req.Env.Dimensions {
if !r.hasDimension(envDimension) {
availableDimensions := make([]string, 0, len(r.Directory()))
for availableDimension := range r.Directory() {
availableDimensions = append(availableDimensions, availableDimension)
}
return errors.New(fmt.Sprint(
"unknown dimension ", envDimension,
" in r.Env must be one of ", availableDimensions,
" repo has ", len(availableDimensions), " dimensions",
))
}
}
return nil
}
func (r *Repo) hasDimension(d string) bool {
_, hasDimension := r.Directory()[d]
return hasDimension
}

249
pkg/repo/repo_test.go Normal file
View File

@ -0,0 +1,249 @@
package repo
import (
"context"
"testing"
"time"
"github.com/foomo/contentserver/pkg/repo/mock"
"github.com/foomo/contentserver/requests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
)
func NewTestRepo(ctx context.Context, l *zap.Logger, url, varDir string) *Repo {
h := NewHistory(l, HistoryWithHistoryLimit(2), HistoryWithHistoryDir(varDir))
r := New(l, url, h)
go r.Start(ctx) //nolint:errcheck
time.Sleep(100 * time.Millisecond)
return r
}
func assertRepoIsEmpty(t *testing.T, r *Repo, empty bool) {
t.Helper()
if empty {
if len(r.Directory()) > 0 {
t.Fatal("directory should have been empty, but is not")
}
} else {
if len(r.Directory()) == 0 {
t.Fatal("directory is empty, but should have been not")
}
}
}
func TestLoad404(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
url = mockServer.URL + "/repo-no-have"
r = NewTestRepo(t.Context(), l, url, varDir)
)
response := r.Update()
if response.Success {
t.Fatal("can not get a repo, if the server responds with a 404")
}
}
func TestLoadBrokenRepo(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-broken-json.json"
r = NewTestRepo(t.Context(), l, server, varDir)
)
response := r.Update()
if response.Success {
t.Fatal("how could we load a broken json")
}
}
func TestLoadRepo(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-ok.json"
r = NewTestRepo(t.Context(), l, server, varDir)
)
assertRepoIsEmpty(t, r, false)
response := r.Update()
assertRepoIsEmpty(t, r, false)
if !response.Success {
t.Fatal("could not load valid repo")
}
if response.Stats.OwnRuntime > response.Stats.RepoRuntime {
t.Fatal("how could all take less time, than me alone")
}
if response.Stats.RepoRuntime < 0.05 {
t.Fatal("the server was too fast")
}
// see what happens if we try to start it up again
// nr := NewTestRepo(l, server, varDir)
// assertRepoIsEmpty(t, nr, false)
}
func BenchmarkLoadRepo(b *testing.B) {
var (
l = zaptest.NewLogger(b)
t = &testing.T{}
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-ok.json"
r = NewTestRepo(b.Context(), l, server, varDir)
)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
response := r.Update()
if len(r.Directory()) == 0 {
b.Fatal("directory is empty, but should have been not")
}
if !response.Success {
b.Fatal("could not load valid repo")
}
}
}
func TestLoadRepoDuplicateUris(t *testing.T) {
var (
l = zaptest.NewLogger(t)
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-duplicate-uris.json"
r = NewTestRepo(t.Context(), l, server, varDir)
)
response := r.Update()
require.False(t, response.Success, "there are duplicates, this repo update should have failed")
assert.Contains(t, response.ErrorMessage, "update dimension")
}
func TestDimensionHygiene(t *testing.T) {
l := zaptest.NewLogger(t)
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-two-dimensions.json"
r := NewTestRepo(t.Context(), l, server, varDir)
response := r.Update()
require.True(t, response.Success, "well those two dimension should be fine")
r.url = mockServer.URL + "/repo-ok.json"
response = r.Update()
require.True(t, response.Success, "it is called repo ok")
assert.Lenf(t, r.Directory(), 1, "directory hygiene failed")
}
func getTestRepo(t *testing.T, path string) *Repo {
t.Helper()
l := zaptest.NewLogger(t)
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + path
r := NewTestRepo(t.Context(), l, server, varDir)
response := r.Update()
require.True(t, response.Success, "well those two dimension should be fine")
return r
}
func TestGetNodes(t *testing.T) {
r := getTestRepo(t, "/repo-two-dimensions.json")
nodesRequest := mock.MakeNodesRequest()
nodes := r.GetNodes(nodesRequest)
testNode, ok := nodes["test"]
require.True(t, ok, "should be a node")
testData, ok := testNode.Item.Data["foo"]
require.True(t, ok, "failed to fetch test data")
t.Log("testData", testData)
}
func TestGetNodesExposeHidden(t *testing.T) {
r := getTestRepo(t, "/repo-ok-exposehidden.json")
nodesRequest := mock.MakeNodesRequest()
nodesRequest.Nodes["test"].ExposeHiddenNodes = true
nodes := r.GetNodes(nodesRequest)
testNode, ok := nodes["test"]
require.True(t, ok, "should be a node")
_, ok = testNode.Item.Data["foo"]
require.True(t, ok, "failed to fetch test data")
require.Len(t, testNode.Nodes, 2)
}
func TestResolveContent(t *testing.T) {
r := getTestRepo(t, "/repo-two-dimensions.json")
contentRequest := mock.MakeValidContentRequest()
siteContent, err := r.GetContent(contentRequest)
require.NoError(t, err)
assert.Equal(t, contentRequest.URI, siteContent.URI, "failed to resolve uri")
}
func TestLinkIds(t *testing.T) {
l := zaptest.NewLogger(t)
var (
mockServer, varDir = mock.GetMockData(t)
server = mockServer.URL + "/repo-link-ok.json"
r = NewTestRepo(t.Context(), l, server, varDir)
response = r.Update()
)
if !response.Success {
t.Fatal("those links should have been fine")
}
r.url = mockServer.URL + "/repo-link-broken.json"
response = r.Update()
if response.Success {
t.Fatal("I do not think so")
}
}
func TestInvalidRequest(t *testing.T) {
r := getTestRepo(t, "/repo-two-dimensions.json")
if r.validateContentRequest(mock.MakeValidContentRequest()) != nil {
t.Fatal("failed validation a valid request")
}
tests := map[string]*requests.Content{}
rEmptyURI := mock.MakeValidContentRequest()
rEmptyURI.URI = ""
tests["empty uri"] = rEmptyURI
rEmptyEnv := mock.MakeValidContentRequest()
rEmptyEnv.Env = nil
tests["empty env"] = rEmptyEnv
rEmptyEnvDimensions := mock.MakeValidContentRequest()
rEmptyEnvDimensions.Env.Dimensions = []string{}
tests["empty env dimensions"] = rEmptyEnvDimensions
// rNodesValidID := mock.MakeValidContentRequest()
// rNodesValidID.Nodes["id-root"].Id = ""
// tests["nodes must have a valid id"] = rNodesValidID
for comment, req := range tests {
if r.validateContentRequest(req) == nil {
t.Fatal(comment, "should have failed")
}
}
}

10
pkg/repo/repodimension.go Normal file
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,96 +0,0 @@
package repo
import (
"io/ioutil"
"os"
"path"
"sort"
"strings"
"time"
"errors"
"fmt"
)
const historyRepoJSONPrefix = "contentserver-repo-"
const historyRepoJSONSuffix = ".json"
const maxHistoryVersions = 20
type history struct {
varDir string
}
func newHistory(varDir string) *history {
return &history{
varDir: varDir,
}
}
func (h *history) add(jsonBytes []byte) error {
// historic file name
filename := path.Join(h.varDir, historyRepoJSONPrefix+time.Now().Format(time.RFC3339Nano)+historyRepoJSONSuffix)
err := ioutil.WriteFile(filename, jsonBytes, 0644)
if err != nil {
return err
}
// current filename
return ioutil.WriteFile(h.getCurrentFilename(), jsonBytes, 0644)
}
func (h *history) getHistory() (files []string, err error) {
files = []string{}
fileInfos, err := ioutil.ReadDir(h.varDir)
if err != nil {
return
}
currentName := h.getCurrentFilename()
for _, f := range fileInfos {
if !f.IsDir() {
filename := f.Name()
if filename != currentName && (strings.HasPrefix(filename, historyRepoJSONPrefix) && strings.HasSuffix(filename, historyRepoJSONSuffix)) {
files = append(files, path.Join(h.varDir, filename))
}
}
}
sort.Sort(sort.Reverse(sort.StringSlice(files)))
return
}
func (h *history) cleanup() error {
files, err := h.getFilesForCleanup(maxHistoryVersions)
if err != nil {
return err
}
for _, f := range files {
err := os.Remove(f)
if err != nil {
return errors.New(fmt.Sprintf("could not remove file %s : %s", f, err.Error()))
}
}
return nil
}
func (h *history) getFilesForCleanup(historyVersions int) (files []string, err error) {
contentFiles, err := h.getHistory()
if err != nil {
return nil, errors.New("could not generate file cleanup list: " + err.Error())
}
if len(contentFiles) > historyVersions {
for i := historyVersions; i < len(contentFiles); i++ {
// ignore current repository file to fall back on
if contentFiles[i] == h.getCurrentFilename() {
continue
}
files = append(files, contentFiles[i])
}
}
return files, nil
}
func (h *history) getCurrentFilename() string {
return path.Join(h.varDir, historyRepoJSONPrefix+"current"+historyRepoJSONSuffix)
}
func (h *history) getCurrent() (jsonBytes []byte, err error) {
return ioutil.ReadFile(h.getCurrentFilename())
}

View File

@ -1,81 +0,0 @@
package repo
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
)
func testHistory() *history {
tempDir, err := ioutil.TempDir(os.TempDir(), "contentserver-history-test")
if err != nil {
panic(err)
}
return newHistory(tempDir)
}
func TestHistoryCurrent(t *testing.T) {
h := testHistory()
test := []byte("test")
h.add(test)
current, err := h.getCurrent()
if err != nil {
t.Fatal(err)
}
if bytes.Compare(current, test) != 0 {
t.Fatal(fmt.Sprintf("expected %q, got %q", string(test), string(current)))
}
}
func TestHistoryCleanup(t *testing.T) {
h := testHistory()
for i := 0; i < 50; i++ {
h.add([]byte(fmt.Sprint(i)))
time.Sleep(time.Millisecond * 5)
}
h.cleanup()
files, err := h.getHistory()
if err != nil {
t.Fatal(err)
}
if len(files) != maxHistoryVersions {
t.Fatal("history too long", len(files), "instead of", maxHistoryVersions)
}
}
func TestHistoryOrder(t *testing.T) {
h := testHistory()
h.varDir = "testdata/order"
files, err := h.getHistory()
if err != nil {
t.Fatal("error not expected")
}
assertStringEqual(t, "testdata/order/contentserver-repo-current.json", files[0])
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-23.json", files[1])
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-22.json", files[2])
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-21.json", files[3])
}
func TestGetFilesForCleanup(t *testing.T) {
h := testHistory()
h.varDir = "testdata/order"
files, err := h.getFilesForCleanup(2)
if err != nil {
t.Fatal("error not expected")
}
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-22.json", files[0])
assertStringEqual(t, "testdata/order/contentserver-repo-2017-10-21.json", files[1])
}
func assertStringEqual(t *testing.T, expected, actual string) {
if expected != actual {
t.Errorf("expected string %s differs from the actual %s", expected, actual)
}
}

View File

@ -1,197 +0,0 @@
package repo
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/log"
)
func (repo *Repo) updateRoutine() {
go func() {
for {
log.Debug("update routine is about to select")
select {
case newDimension := <-repo.updateChannel:
log.Debug("update routine received a new dimension: " + newDimension.Dimension)
err := repo._updateDimension(newDimension.Dimension, newDimension.Node)
log.Debug("update routine received result")
if err != nil {
log.Debug(" update routine error: " + err.Error())
}
repo.updateDoneChannel <- err
}
}
}()
}
func (repo *Repo) updateDimension(dimension string, node *content.RepoNode) error {
repo.updateChannel <- &repoDimension{
Dimension: dimension,
Node: node,
}
return <-repo.updateDoneChannel
}
// do not call directly, but only through channel
func (repo *Repo) _updateDimension(dimension string, newNode *content.RepoNode) error {
newNode.WireParents()
newDirectory := make(map[string]*content.RepoNode)
newURIDirectory := make(map[string]*content.RepoNode)
err := builDirectory(newNode, newDirectory, newURIDirectory)
if err != nil {
return errors.New("update dimension \"" + dimension + "\" failed when building its directory:: " + err.Error())
}
err = wireAliases(newDirectory)
if err != nil {
return err
}
repo.Directory[dimension] = &Dimension{
Node: newNode,
Directory: newDirectory,
URIDirectory: newURIDirectory,
}
return nil
}
func builDirectory(dirNode *content.RepoNode, directory map[string]*content.RepoNode, uRIDirectory map[string]*content.RepoNode) error {
log.Debug("repo.buildDirectory: " + dirNode.ID)
existingNode, ok := directory[dirNode.ID]
if ok {
return errors.New("duplicate node with id:" + existingNode.ID)
}
directory[dirNode.ID] = dirNode
//todo handle duplicate uris
if _, thereIsAnExistingURINode := uRIDirectory[dirNode.URI]; thereIsAnExistingURINode {
return errors.New("duplicate uri: " + dirNode.URI + " (bad node id: " + dirNode.ID + ")")
}
uRIDirectory[dirNode.URI] = dirNode
for _, childNode := range dirNode.Nodes {
err := builDirectory(childNode, directory, uRIDirectory)
if err != nil {
return err
}
}
return nil
}
func wireAliases(directory map[string]*content.RepoNode) error {
for _, repoNode := range directory {
if len(repoNode.LinkID) > 0 {
if destinationNode, ok := directory[repoNode.LinkID]; ok {
repoNode.URI = destinationNode.URI
} else {
return errors.New("that link id points nowhere " + repoNode.LinkID + " from " + repoNode.ID)
}
}
}
return nil
}
func loadNodesFromJSON(jsonBytes []byte) (nodes map[string]*content.RepoNode, err error) {
nodes = make(map[string]*content.RepoNode)
err = json.Unmarshal(jsonBytes, &nodes)
return nodes, err
}
func (repo *Repo) tryToRestoreCurrent() error {
currentJSONBytes, err := repo.history.getCurrent()
if err != nil {
return err
}
return repo.loadJSONBytes(currentJSONBytes)
}
func get(URL string) (data []byte, err error) {
response, err := http.Get(URL)
if err != nil {
return data, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return data, fmt.Errorf("Bad HTTP Response: %q", response.Status)
}
return ioutil.ReadAll(response.Body)
}
func (repo *Repo) update() (repoRuntime int64, jsonBytes []byte, err error) {
startTimeRepo := time.Now().UnixNano()
jsonBytes, err = get(repo.server)
repoRuntime = time.Now().UnixNano() - startTimeRepo
if err != nil {
// we have no json to load - the repo server did not reply
log.Debug("we have no json to load - the repo server did not reply", err)
return repoRuntime, jsonBytes, err
}
log.Debug("loading json from: "+repo.server, string(jsonBytes))
nodes, err := loadNodesFromJSON(jsonBytes)
if err != nil {
// could not load nodes from json
return repoRuntime, jsonBytes, err
}
err = repo.loadNodes(nodes)
if err != nil {
// repo failed to load nodes
return repoRuntime, jsonBytes, err
}
return repoRuntime, jsonBytes, nil
}
func (repo *Repo) loadJSONBytes(jsonBytes []byte) error {
nodes, err := loadNodesFromJSON(jsonBytes)
if err != nil {
log.Debug("could not parse json", string(jsonBytes))
return err
}
err = repo.loadNodes(nodes)
if err == nil {
historyErr := repo.history.add(jsonBytes)
if historyErr != nil {
log.Warning("could not add valid json to history:" + historyErr.Error())
} else {
log.Record("added valid json to history")
}
cleanUpErr := repo.history.cleanup()
if cleanUpErr != nil {
log.Warning("an error occured while cleaning up my history:", cleanUpErr)
} else {
log.Record("cleaned up history")
}
}
return err
}
func (repo *Repo) loadNodes(newNodes map[string]*content.RepoNode) error {
newDimensions := []string{}
for dimension, newNode := range newNodes {
newDimensions = append(newDimensions, dimension)
log.Debug("loading nodes for dimension " + dimension)
loadErr := repo.updateDimension(dimension, newNode)
if loadErr != nil {
log.Debug(" failed to load " + dimension + ": " + loadErr.Error())
return loadErr
}
}
dimensionIsValid := func(dimension string) bool {
for _, newDimension := range newDimensions {
if dimension == newDimension {
return true
}
}
return false
}
// we need to throw away orphaned dimensions
for dimension := range repo.Directory {
if !dimensionIsValid(dimension) {
log.Notice("removing orphaned dimension:" + dimension)
delete(repo.Directory, dimension)
}
}
return nil
}

View File

@ -1,328 +0,0 @@
package repo
import (
"errors"
"fmt"
"strings"
"time"
"github.com/foomo/contentserver/content"
"github.com/foomo/contentserver/log"
"github.com/foomo/contentserver/requests"
"github.com/foomo/contentserver/responses"
)
// Dimension dimension in a repo
type Dimension struct {
Directory map[string]*content.RepoNode
URIDirectory map[string]*content.RepoNode
Node *content.RepoNode
}
// Repo content repositiory
type Repo struct {
server string
Directory map[string]*Dimension
updateChannel chan *repoDimension
updateDoneChannel chan error
history *history
}
type repoDimension struct {
Dimension string
Node *content.RepoNode
}
// NewRepo constructor
func NewRepo(server string, varDir string) *Repo {
log.Notice("creating new repo for " + server)
log.Notice(" using var dir:" + varDir)
repo := &Repo{
server: server,
Directory: map[string]*Dimension{},
history: newHistory(varDir),
updateChannel: make(chan *repoDimension),
updateDoneChannel: make(chan error),
}
go repo.updateRoutine()
log.Record("trying to restore pervious state")
restoreErr := repo.tryToRestoreCurrent()
if restoreErr != nil {
log.Record(" could not restore previous repo content:" + restoreErr.Error())
} else {
log.Record(" restored previous repo content")
}
return repo
}
// GetURIs get many uris at once
func (repo *Repo) GetURIs(dimension string, ids []string) map[string]string {
uris := map[string]string{}
for _, id := range ids {
uris[id] = repo.getURI(dimension, id)
}
return uris
}
// GetNodes get nodes
func (repo *Repo) GetNodes(r *requests.Nodes) map[string]*content.Node {
return repo.getNodes(r.Nodes, r.Env)
}
func (repo *Repo) getNodes(nodeRequests map[string]*requests.Node, env *requests.Env) map[string]*content.Node {
nodes := map[string]*content.Node{}
path := []*content.Item{}
for nodeName, nodeRequest := range nodeRequests {
log.Debug(" adding node " + nodeName + " " + nodeRequest.ID)
groups := env.Groups
if len(nodeRequest.Groups) > 0 {
groups = nodeRequest.Groups
}
dimensionNode, ok := repo.Directory[nodeRequest.Dimension]
nodes[nodeName] = nil
if !ok && nodeRequest.Dimension == "" {
log.Debug(" could not get dimension root node for dimension " + nodeRequest.Dimension)
for _, dimension := range env.Dimensions {
dimensionNode, ok = repo.Directory[dimension]
if ok {
log.Debug(" searched for root node in env.dimension " + dimension + " with success")
break
}
log.Debug(" searched for root node in env.dimension " + dimension + " without success")
}
}
if !ok {
log.Warning("could not get dimension root node for nodeRequest.Dimension: " + nodeRequest.Dimension)
continue
}
treeNode, ok := dimensionNode.Directory[nodeRequest.ID]
if ok {
nodes[nodeName] = repo.getNode(treeNode, nodeRequest.Expand, nodeRequest.MimeTypes, path, 0, groups, nodeRequest.DataFields)
} else {
log.Warning("you are requesting an invalid tree node for " + nodeName + " : " + nodeRequest.ID)
}
}
return nodes
}
// GetContent resolves content and fetches nodes in one call. It combines those
// two tasks for performance reasons.
//
// In the first step it uses r.URI to look up content in all given
// r.Env.Dimensions of repo.Directory.
//
// In the second step it collects the requested nodes.
//
// those two steps are independent.
func (repo *Repo) GetContent(r *requests.Content) (c *content.SiteContent, err error) {
// add more input validation
err = repo.validateContentRequest(r)
if err != nil {
log.Debug("repo.GetContent invalid request", err)
return
}
log.Debug("repo.GetContent: ", r.URI)
c = content.NewSiteContent()
resolved, resolvedURI, resolvedDimension, node := repo.resolveContent(r.Env.Dimensions, r.URI)
if resolved {
if !node.CanBeAccessedByGroups(r.Env.Groups) {
log.Notice("401 for " + r.URI)
c.Status = content.StatusForbidden
} else {
log.Notice("200 for " + r.URI)
c.Status = content.StatusOk
c.Data = node.Data
}
c.MimeType = node.MimeType
c.Dimension = resolvedDimension
c.URI = resolvedURI
c.Item = node.ToItem([]string{})
c.Path = node.GetPath()
// fetch URIs for all dimensions
uris := make(map[string]string)
for dimensionName := range repo.Directory {
uris[dimensionName] = repo.getURI(dimensionName, node.ID)
}
c.URIs = uris
} else {
log.Notice("404 for " + r.URI)
c.Status = content.StatusNotFound
c.Dimension = r.Env.Dimensions[0]
}
if log.SelectedLevel == log.LevelDebug {
log.Debug(fmt.Sprintf("resolved: %v, uri: %v, dim: %v, n: %v", resolved, resolvedURI, resolvedDimension, node))
}
if resolved == false {
log.Debug("repo.GetContent", r.URI, "could not be resolved falling back to default dimension", r.Env.Dimensions[0])
// r.Env.Dimensions is validated => we can access it
resolvedDimension = r.Env.Dimensions[0]
}
// add navigation trees
for _, node := range r.Nodes {
if node.Dimension == "" {
node.Dimension = resolvedDimension
}
}
c.Nodes = repo.getNodes(r.Nodes, r.Env)
return c, nil
}
// GetRepo get the whole repo in all dimensions
func (repo *Repo) GetRepo() map[string]*content.RepoNode {
response := make(map[string]*content.RepoNode)
for dimensionName, dimension := range repo.Directory {
response[dimensionName] = dimension.Node
}
return response
}
// Update - reload contents of repository with json from repo.server
func (repo *Repo) Update() (updateResponse *responses.Update) {
floatSeconds := func(nanoSeconds int64) float64 {
return float64(float64(nanoSeconds) / float64(1000000000.0))
}
startTime := time.Now().UnixNano()
updateRepotime, jsonBytes, updateErr := repo.update()
updateResponse = &responses.Update{}
updateResponse.Stats.RepoRuntime = floatSeconds(updateRepotime)
if updateErr != nil {
updateResponse.Success = false
updateResponse.Stats.NumberOfNodes = -1
updateResponse.Stats.NumberOfURIs = -1
// let us try to restore the world from a file
log.Error("could not update repository:" + updateErr.Error())
updateResponse.ErrorMessage = updateErr.Error()
restoreErr := repo.tryToRestoreCurrent()
if restoreErr != nil {
log.Error("failed to restore preceding repo version: " + restoreErr.Error())
} else {
log.Record("restored current repo from local history")
}
} else {
updateResponse.Success = true
// persist the currently loaded one
historyErr := repo.history.add(jsonBytes)
if historyErr != nil {
log.Warning("could not persist current repo in history: " + historyErr.Error())
}
// add some stats
for dimension := range repo.Directory {
updateResponse.Stats.NumberOfNodes += len(repo.Directory[dimension].Directory)
updateResponse.Stats.NumberOfURIs += len(repo.Directory[dimension].URIDirectory)
}
}
updateResponse.Stats.OwnRuntime = floatSeconds(time.Now().UnixNano()-startTime) - updateResponse.Stats.RepoRuntime
return updateResponse
}
// resolveContent find content in a repository
func (repo *Repo) resolveContent(dimensions []string, URI string) (resolved bool, resolvedURI string, resolvedDimension string, repoNode *content.RepoNode) {
parts := strings.Split(URI, content.PathSeparator)
resolved = false
resolvedURI = ""
resolvedDimension = ""
repoNode = nil
log.Debug("repo.ResolveContent: " + URI)
for i := len(parts); i > 0; i-- {
testURI := strings.Join(parts[0:i], content.PathSeparator)
if testURI == "" {
testURI = content.PathSeparator
}
for _, dimension := range dimensions {
if d, ok := repo.Directory[dimension]; ok {
log.Debug(" testing[" + dimension + "]: " + testURI)
if repoNode, ok := d.URIDirectory[testURI]; ok {
resolved = true
log.Debug(" found => " + testURI)
log.Debug(" destination " + fmt.Sprint(repoNode.DestinationID))
if len(repoNode.DestinationID) > 0 {
if destionationNode, destinationNodeOk := d.Directory[repoNode.DestinationID]; destinationNodeOk {
repoNode = destionationNode
}
}
return true, testURI, dimension, repoNode
}
}
}
}
return
}
const maxGetURIForNodeRecursionLevel = 1000
func (repo *Repo) getURIForNode(dimension string, repoNode *content.RepoNode, recursionLevel int64) (uri string) {
if len(repoNode.LinkID) == 0 {
uri = repoNode.URI
return
}
linkedNode, ok := repo.Directory[dimension].Directory[repoNode.LinkID]
if ok {
if recursionLevel > maxGetURIForNodeRecursionLevel {
log.Error("maxGetURIForNodeRecursionLevel reached for", repoNode.ID, "link id", repoNode.LinkID, "in dimension", dimension)
return ""
}
return repo.getURIForNode(dimension, linkedNode, recursionLevel+1)
}
return
}
func (repo *Repo) getURI(dimension string, id string) string {
repoNode, ok := repo.Directory[dimension].Directory[id]
if ok {
return repo.getURIForNode(dimension, repoNode, 0)
}
return ""
}
func (repo *Repo) getNode(repoNode *content.RepoNode, expanded bool, mimeTypes []string, path []*content.Item, level int, groups []string, dataFields []string) *content.Node {
node := content.NewNode()
node.Item = repoNode.ToItem(dataFields)
log.Debug("repo.GetNode: " + repoNode.ID)
for _, childID := range repoNode.Index {
childNode := repoNode.Nodes[childID]
if (level == 0 || expanded || !expanded && childNode.InPath(path)) && !childNode.Hidden && childNode.CanBeAccessedByGroups(groups) && childNode.IsOneOfTheseMimeTypes(mimeTypes) {
node.Nodes[childID] = repo.getNode(childNode, expanded, mimeTypes, path, level+1, groups, dataFields)
node.Index = append(node.Index, childID)
}
}
return node
}
func (repo *Repo) validateContentRequest(req *requests.Content) (err error) {
if req == nil {
return errors.New("request must not be nil")
}
if len(req.URI) == 0 {
return errors.New("request URI must not be empty")
}
if req.Env == nil {
return errors.New("request.Env must not be nil")
}
if len(req.Env.Dimensions) == 0 {
return errors.New("request.Env.Dimensions must not be empty")
}
for _, envDimension := range req.Env.Dimensions {
if !repo.hasDimension(envDimension) {
availableDimensions := []string{}
for availableDimension := range repo.Directory {
availableDimensions = append(availableDimensions, availableDimension)
}
return fmt.Errorf("unknown dimension %q in r.Env must be one of %q", envDimension, availableDimensions)
}
}
return nil
}
func (repo *Repo) hasDimension(d string) bool {
_, hasDimension := repo.Directory[d]
return hasDimension
}
func uriKeyForState(state string, uri string) string {
return state + "-" + uri
}

View File

@ -1,186 +0,0 @@
package repo
import (
"strings"
"testing"
"github.com/foomo/contentserver/repo/mock"
"github.com/foomo/contentserver/requests"
)
func assertRepoIsEmpty(t *testing.T, r *Repo, empty bool) {
if empty {
if len(r.Directory) > 0 {
t.Fatal("directory should have been empty, but is not")
}
} else {
if len(r.Directory) == 0 {
t.Fatal("directory should not have been empty, but it is")
}
}
}
func TestLoad404(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-no-have"
r := NewRepo(server, varDir)
response := r.Update()
if response.Success {
t.Fatal("can not get a repo, if the server responds with a 404")
}
}
func TestLoadBrokenRepo(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-broken-json.json"
r := NewRepo(server, varDir)
response := r.Update()
if response.Success {
t.Fatal("how could we load a broken json")
}
}
func TestLoadRepo(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-ok.json"
r := NewRepo(server, varDir)
assertRepoIsEmpty(t, r, true)
response := r.Update()
assertRepoIsEmpty(t, r, false)
if response.Success == false {
t.Fatal("could not load valid repo")
}
if response.Stats.OwnRuntime > response.Stats.RepoRuntime {
t.Fatal("how could all take less time, than me alone")
}
if response.Stats.RepoRuntime < float64(0.05) {
t.Fatal("the server was too fast")
}
// see what happens if we try to start it up again
nr := NewRepo(server, varDir)
assertRepoIsEmpty(t, nr, false)
}
func TestLoadRepoDuplicateUris(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-duplicate-uris.json"
r := NewRepo(server, varDir)
response := r.Update()
if response.Success {
t.Fatal("there are duplicates, this repo update should have failed")
}
if !strings.Contains(response.ErrorMessage, "update dimension") {
t.Fatal("error message not as expected")
}
}
func TestDimensionHygiene(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-two-dimensions.json"
r := NewRepo(server, varDir)
response := r.Update()
if !response.Success {
t.Fatal("well those two dimension should be fine")
}
r.server = mockServer.URL + "/repo-ok.json"
response = r.Update()
if !response.Success {
t.Fatal("wtf it is called repo ok")
}
if len(r.Directory) != 1 {
t.Fatal("directory hygiene failed")
}
}
func getTestRepo(path string, t *testing.T) *Repo {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + path
r := NewRepo(server, varDir)
response := r.Update()
if !response.Success {
t.Fatal("well those two dimension should be fine")
}
return r
}
func TestGetNodes(t *testing.T) {
r := getTestRepo("/repo-two-dimensions.json", t)
nodesRequest := mock.MakeNodesRequest()
nodes := r.GetNodes(nodesRequest)
testNode, ok := nodes["test"]
if !ok {
t.Fatal("wtf that should be a node")
}
testData, ok := testNode.Item.Data["foo"]
t.Log("testData", testData)
if !ok {
t.Fatal("failed to fetch test data")
}
}
func TestResolveContent(t *testing.T) {
r := getTestRepo("/repo-two-dimensions.json", t)
contentRequest := mock.MakeValidContentRequest()
siteContent, err := r.GetContent(contentRequest)
if siteContent.URI != contentRequest.URI {
t.Fatal("failed to resolve uri")
}
if err != nil {
t.Fatal(err)
}
}
func TestLinkIds(t *testing.T) {
mockServer, varDir := mock.GetMockData(t)
server := mockServer.URL + "/repo-link-ok.json"
r := NewRepo(server, varDir)
response := r.Update()
if !response.Success {
t.Fatal("those links should have been fine")
}
r.server = mockServer.URL + "/repo-link-broken.json"
response = r.Update()
if response.Success {
t.Fatal("I do not think so")
}
}
func TestInvalidRequest(t *testing.T) {
r := getTestRepo("/repo-two-dimensions.json", t)
if r.validateContentRequest(mock.MakeValidContentRequest()) != nil {
t.Fatal("failed validation a valid request")
}
tests := map[string]*requests.Content{}
rEmptyURI := mock.MakeValidContentRequest()
rEmptyURI.URI = ""
tests["empty uri"] = rEmptyURI
rEmptyEnv := mock.MakeValidContentRequest()
rEmptyEnv.Env = nil
tests["empty env"] = rEmptyEnv
rEmptyEnvDimensions := mock.MakeValidContentRequest()
rEmptyEnvDimensions.Env.Dimensions = []string{}
tests["empty env dimensions"] = rEmptyEnvDimensions
//rNodesValidID := mock.MakeValidContentRequest()
//rNodesValidID.Nodes["id-root"].Id = ""
//tests["nodes must have a valid id"] = rNodesValidID
for comment, req := range tests {
if r.validateContentRequest(req) == nil {
t.Fatal(comment, "should have failed")
}
}
}

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

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