Compare commits

...

92 Commits

Author SHA1 Message Date
Amir Raminfar 7cf67eb99a chore: release v8.11.2 2025-02-15 09:49:01 -08:00
renovate[bot] 869d5e5002 fix(deps): update all non-major dependencies (#3633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 17:48:48 +00:00
Amir Raminfar 392dfafc3d chore: updates docs 2025-02-15 09:37:57 -08:00
Amir Raminfar 21fe5b84f2 feat: supports namespaces properly in k8s mode (#3631) 2025-02-15 09:35:32 -08:00
Amir Raminfar 65fa04c877 chore: adds metrics to docs
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-14 07:46:58 -08:00
renovate[bot] 3a352a7320 fix(deps): update all non-major dependencies (#3629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 15:01:24 +00:00
Amir Raminfar 3bdc9c04f8 chore: release v8.11.1
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-13 16:41:06 -08:00
Amir Raminfar 6e3c811974 fix: fixes broken nil pointer. fixes #3626 (#3627)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-13 13:31:25 -08:00
Amir Raminfar ddff9e96a2 chore: updates static go version 2025-02-13 08:35:11 -08:00
Amir Raminfar 1daf813e1b chore: release v8.11.0 2025-02-13 08:21:04 -08:00
renovate[bot] 09a169851b fix(deps): update all non-major dependencies (#3624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 16:19:57 +00:00
Amir Raminfar 1d5ddee1ae docs: adds k8s in docs (#3623) 2025-02-13 08:19:37 -08:00
Amir Raminfar b7e9871382 chore: adds tests cases back
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-12 17:02:24 -08:00
Amir Raminfar 9e028b9916 chore: improves tests 2025-02-12 15:55:49 -08:00
renovate[bot] 78c0e5fd7f fix(deps): update dependency pinia to ^3.0.1 (#3619)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 19:59:28 +00:00
Amir Raminfar 4861503488 chore: updates to go v1.24 (#3622) 2025-02-12 11:53:52 -08:00
Amir Raminfar 115b0e475d docs: adds k8s in docs (#3620) 2025-02-12 15:53:56 +00:00
Amir Raminfar 6d928815df chore: updates pinia (#3618)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-11 23:00:43 +00:00
Amir Raminfar 2a70d0b458 chore: add metrics to config 2025-02-11 14:54:37 -08:00
Amir Raminfar 8c9c9a83af fix: fixes deadlocks in store (#3617)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-11 12:32:56 -08:00
Amir Raminfar 571de4084e chore: removes survey 2025-02-11 09:57:36 -08:00
Amir Raminfar 5abf7e4a9d docs: remove banner for survey 2025-02-11 09:22:27 -08:00
renovate[bot] d580e9447b chore(deps): update pnpm to v10.3.0 (#3616)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 03:29:31 +00:00
Amir Raminfar 877faaa740 chore: fixes broken interface 2025-02-10 14:30:37 -08:00
Amir Raminfar 1d4698d77a chore: adds error for agent 2025-02-10 14:19:05 -08:00
Amir Raminfar 13da2a4222 feat: supports k8s cluster (#3599)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-10 09:29:39 -08:00
renovate[bot] 209ce312d4 fix(deps): update all non-major dependencies to v4.0.6 (#3615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 17:21:40 +00:00
Amir Raminfar 6da36c4c0d chore: release v8.10.7
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-09 14:28:09 -08:00
renovate[bot] d9d93a8147 fix(deps): update all non-major dependencies (#3612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-09 22:19:39 +00:00
Amir Raminfar eba87fa49e fix: updates default timeout and adds user agent. see #3610 (#3611) 2025-02-09 14:13:31 -08:00
Amir Raminfar 7ae409981c chore: removes flaky test
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-09 07:15:17 -08:00
renovate[bot] 20fb7984d2 fix(deps): update all non-major dependencies (#3607)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 05:42:10 +00:00
Amir Raminfar 17c15b65a2 Merge branch 'patch-v8.10.6'
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-02-05 10:44:00 -08:00
Amir Raminfar 9ce736a448 chore: release v8.10.6 2025-02-05 10:36:10 -08:00
Amir Raminfar 10c95124e4 fixes build 2025-02-05 10:34:16 -08:00
Amir Raminfar 5e02142f77 Revert "chore: release v8.10.6"
This reverts commit f9585cc656.
2025-02-05 10:29:40 -08:00
Amir Raminfar f9585cc656 chore: release v8.10.6 2025-02-05 10:26:26 -08:00
Amir Raminfar 6da7f6dffd fix: fixes broken download link when host name has : 2025-02-05 10:23:34 -08:00
renovate[bot] 2de739141d fix(deps): update all non-major dependencies (#3603)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-05 17:45:33 +00:00
renovate[bot] 585e1edfee fix(deps): update all non-major dependencies (#3596)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-04 16:31:19 +00:00
Amir Raminfar bf81852003 chore: tries to update pnpm (#3598) 2025-02-04 08:26:12 -08:00
Amir Raminfar 5f73b41c57 chore: refactors to be more generic (#3594)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-02-03 12:42:09 -08:00
renovate[bot] 9f7b17f4ec fix(deps): update dependency fuse.js to ^7.1.0 (#3593)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 01:13:30 +00:00
renovate[bot] 6c292be943 fix(deps): update all non-major dependencies (#3592)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-02 18:33:04 +00:00
renovate[bot] 21ae6eda8b fix(deps): update all non-major dependencies to v4.0.3 (#3591)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-01 18:38:58 +00:00
renovate[bot] b732844e7b fix(deps): update all non-major dependencies (#3588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 23:46:07 +00:00
Amir Raminfar 1b38f37ac2 chore: remove data
Test / JavaScript Tests (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-01-30 08:26:10 -08:00
renovate[bot] c20ce29f5c fix(deps): update all non-major dependencies (#3584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 16:10:09 +00:00
Amir Raminfar 47e64bb57a fix: fixes --menu-active-bg 2025-01-30 08:05:18 -08:00
renovate[bot] b9a5262257 fix(deps): update all non-major dependencies (#3583)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-29 23:50:02 +00:00
Amir Raminfar 47204b33c4 chore: updates npm modules
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2025-01-28 10:38:02 -08:00
renovate[bot] 162f9ea580 fix(deps): update module github.com/samber/lo to v1.49.0 (#3579)
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 00:42:58 +00:00
renovate[bot] 2381a8b8a3 fix(deps): update all non-major dependencies (#3578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-26 17:50:29 +00:00
Amir Raminfar 8fbbfaf043 feat: adds new menu for custom groups (#3573)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-01-25 10:06:58 -08:00
renovate[bot] 3d077b9503 chore(deps): update dependency bumpp to v10 (#3577)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-01-25 15:57:47 +00:00
renovate[bot] 68ea86c55b fix(deps): update all non-major dependencies (#3576)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-24 21:48:43 +00:00
Amir Raminfar ad5c1c4452 fix(docs): fixes label
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-01-23 11:23:34 -08:00
Amir Raminfar 0efd98e999 chore: fixes snapshots 2025-01-23 09:28:37 -08:00
renovate[bot] ff72431a72 fix(deps): update all non-major dependencies (#3572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 09:26:24 -08:00
Amir Raminfar 6d2ed53629 chore: release v8.10.5 2025-01-22 10:01:03 -08:00
Amir Raminfar 992e1a27ae chore: generate 2025-01-22 10:00:53 -08:00
Amir Raminfar 2378a1f115 chore: adds survey as link (#3571) 2025-01-22 17:58:06 +00:00
Amir Raminfar 3b122564d9 chore: adds ref 2025-01-22 09:29:20 -08:00
renovate[bot] a783f7060c fix(deps): update all non-major dependencies (#3570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 16:54:37 +00:00
renovate[bot] 76e5c63436 fix(deps): update all non-major dependencies to v4.0.0 (#3569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 22:16:23 +00:00
Amir Raminfar 5a17a07b8f chore: docs fixes x again 2025-01-21 11:27:04 -08:00
Amir Raminfar d1fc9b750b chore: docs fixes x again 2025-01-21 10:27:12 -08:00
Amir Raminfar 87a59939dd chore: fixes close button 2025-01-21 09:59:35 -08:00
Amir Raminfar 3a159cb89b fix: only truncate right the container name and not hostname (#3567) 2025-01-21 08:40:03 -08:00
Amir Raminfar 84ec46c400 docs: adds banner for survey (#3566) 2025-01-21 08:19:00 -08:00
renovate[bot] 0c4ca6d605 fix(deps): update all non-major dependencies (#3565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 15:55:12 +00:00
renovate[bot] db5d36c89b fix(deps): update all non-major dependencies (#3564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 17:46:49 +00:00
renovate[bot] 8cdacf608a fix(deps): update all non-major dependencies (#3562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 15:23:47 +00:00
renovate[bot] 78487a553c chore(deps): update all non-major dependencies (#3561)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 19:32:03 +00:00
renovate[bot] 8da67482d8 chore(deps): update dependency vitest to v3 (#3560)
Push container / Push branches and PRs (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 17:10:57 +00:00
renovate[bot] 60914f3208 chore(deps): update dependency @apache-arrow/esnext-esm to v19 (#3559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 08:36:07 -08:00
renovate[bot] 0dc2a391df chore(deps): update all non-major dependencies (#3558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 16:35:16 +00:00
renovate[bot] 4cfd3d0cfa fix(deps): update all non-major dependencies (#3556)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 17:29:46 +00:00
renovate[bot] 9752504097 fix(deps): update all non-major dependencies (#3551)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 16:01:45 +00:00
Amir Raminfar f08f2455de chore: release v8.10.4
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-01-13 19:08:55 -08:00
Amir Raminfar b2ea1c592a fix: fixes prefix 2025-01-13 19:08:08 -08:00
Amir Raminfar 6f89a1e70b chore: release v8.10.3 2025-01-13 18:35:59 -08:00
Amir Raminfar 93ce48ebf3 chore: fixes release 2025-01-13 18:35:48 -08:00
Amir Raminfar 0412581b6f Revert "chore: release v8.10.3"
This reverts commit 285d0ef0f4.
2025-01-13 18:31:52 -08:00
Amir Raminfar 285d0ef0f4 chore: release v8.10.3 2025-01-13 18:26:07 -08:00
Webysther Sperandio be313a68a3 feat: add more tags to same release (#3550) 2025-01-13 18:24:00 -08:00
Amir Raminfar 14ddd1c4e8 fix: adds more translations (#3548) 2025-01-13 16:10:19 -08:00
Amir Raminfar 368646a7a3 feat: improves toasts for mobile (#3547)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Typecheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
2025-01-13 19:16:17 +00:00
renovate[bot] 691a89660e fix(deps): update dependency unplugin-vue-macros to ^2.13.8 (#3546)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 01:24:48 +00:00
Amir Raminfar d03844b488 feat: improves toasts for mobile (#3545) 2025-01-12 14:04:57 -08:00
Amir Raminfar 3f860805f5 fix: moves to the right card depending on route (#3544) 2025-01-12 13:53:53 -08:00
renovate[bot] e8eef82b1c fix(deps): update dependency splitpanes to ^3.1.8 (#3541)
Push container / Push branches and PRs (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Test / Typecheck (push) Waiting to run
Test / JavaScript Tests (push) Waiting to run
Test / Go Tests (push) Waiting to run
Test / Go Staticcheck (push) Waiting to run
Test / Integration Tests (push) Waiting to run
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-12 01:01:52 +00:00
128 changed files with 4675 additions and 3573 deletions
+7 -2
View File
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
@@ -111,12 +111,17 @@ jobs:
images: |
amir20/dozzle
ghcr.io/amir20/dozzle
tags: |
type=raw,value=latest
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
type=semver,pattern={{major}},prefix=v
- name: Writing certs to file
run: |
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.11.0
uses: docker/build-push-action@v6.13.0
with:
push: true
context: .
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Login to DockerHub
uses: docker/login-action@v3.3.0
with:
@@ -39,7 +39,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.11.0
uses: docker/build-push-action@v6.13.0
with:
context: .
push: true
+1 -1
View File
@@ -77,7 +77,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.24"
check-latest: true
- name: Generate dependencies
run: make fake_assets shared_key.pem shared_cert.pem
+2 -2
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:23.6.0-alpine AS node
FROM --platform=$BUILDPLATFORM node:23.8.0-alpine AS node
RUN corepack enable
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.23.4-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
+4 -2
View File
@@ -162,6 +162,7 @@ declare global {
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAnnouncements: typeof import('./stores/announcements')['useAnnouncements']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@@ -192,6 +193,7 @@ declare global {
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useContainerStream: typeof import('./composable/eventStreams')['useContainerStream']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -287,7 +289,6 @@ declare global {
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useReleases: typeof import('./stores/releases')['useReleases']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
@@ -551,6 +552,7 @@ declare module 'vue' {
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAnnouncements: UnwrapRef<typeof import('./stores/announcements')['useAnnouncements']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@@ -581,6 +583,7 @@ declare module 'vue' {
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useContainerStream: UnwrapRef<typeof import('./composable/eventStreams')['useContainerStream']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
@@ -676,7 +679,6 @@ declare module 'vue' {
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
+4 -1
View File
@@ -7,10 +7,12 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Announcements: typeof import('./components/Announcements.vue')['default']
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:copyFile': typeof import('~icons/carbon/copy-file')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:logoKubernetes': typeof import('~icons/carbon/logo-kubernetes')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
@@ -39,6 +41,7 @@ declare module 'vue' {
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
GroupMenu: typeof import('./components/GroupMenu.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
@@ -99,7 +102,6 @@ declare module 'vue' {
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
Popup: typeof import('./components/Popup.vue')['default']
RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default']
Releases: typeof import('./components/Releases.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollableView: typeof import('./components/ScrollableView.vue')['default']
@@ -119,6 +121,7 @@ declare module 'vue' {
SwarmMenu: typeof import('./components/SwarmMenu.vue')['default']
Tag: typeof import('./components/common/Tag.vue')['default']
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
ToastModal: typeof import('./components/common/ToastModal.vue')['default']
Toggle: typeof import('./components/common/Toggle.vue')['default']
ViewerWithSource: typeof import('./components/LogViewer/ViewerWithSource.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
+115
View File
@@ -0,0 +1,115 @@
<template>
<Dropdown class="dropdown-end" @closed="releaseSeen = mostRecent?.tag ?? config.version">
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
<template v-if="announcements.length > 0 && releaseSeen != mostRecent?.tag">
<span class="bg-red absolute top-0 right-px size-2 animate-ping rounded-full opacity-75"></span>
<span class="bg-red absolute top-0 right-px size-2 rounded-full"></span>
</template>
</template>
<template #content>
<div class="w-72">
<ul class="space-y-4 p-2">
<li v-for="release in announcements" v-if="announcements.length > 0">
<template v-if="release.announcement">
<div class="flex items-baseline gap-1">
<carbon:information class="text-info self-center" />
<a
:href="release.htmlUrl"
class="link-primary text-lg font-bold"
target="_blank"
rel="noreferrer noopener"
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
</div>
<div class="text-base-content/80 text-sm">
{{ release.body }}
</div>
</template>
<template v-else>
<div class="flex items-baseline gap-1">
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
<a
:href="release.htmlUrl"
class="link-primary text-lg font-bold"
target="_blank"
rel="noreferrer noopener"
>
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="release.createdAt" /></span>
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.latest">
{{ $t("releases.latest") }}
</Tag>
</div>
<div class="text-base-content/80 text-sm">
{{ summary(release) }}
</div>
</template>
</li>
<li v-else>
<div class="text-base-content/80 text-sm">
{{ $t("releases.no_releases") }}
</div>
</li>
</ul>
</div>
</template>
</Dropdown>
</template>
<script setup lang="ts">
import { useAnnouncements } from "@/stores/announcements";
const { announcements, mostRecent } = useAnnouncements();
const { t } = useI18n();
const releaseSeen = useProfileStorage("releaseSeen", config.version);
function summary(release: { features: number; bugFixes: number; breaking: number }) {
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
return t("releases.three_parts", {
first: t("releases.breaking", { count: release.breaking }),
second: t("releases.features", { count: release.features }),
third: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.bugFixes > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.bugFixes > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.bugFixes", { count: release.bugFixes }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.features > 0) {
return t("releases.features", { count: release.features });
}
if (release.bugFixes > 0) {
return t("releases.bugFixes", { count: release.bugFixes });
}
if (release.breaking > 0) {
return t("releases.breaking", { count: release.breaking });
}
}
</script>
<style scoped></style>
@@ -169,7 +169,7 @@ const downloadParams = computed(() =>
const downloadUrl = computed(() =>
withBase(
`/api/containers/${container.host}:${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
`/api/containers/${container.host}~${container.id}/download?${new URLSearchParams(downloadParams.value).toString()}`,
),
);
+29
View File
@@ -0,0 +1,29 @@
<template>
<ul class="menu w-full p-0 text-[0.95rem]">
<li v-if="customGroups.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:bounding-box-fill />
{{ $t("label.group-menu") }}
</summary>
<ul>
<li v-for="group in customGroups" :key="group.name">
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">
{{ group.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
<script lang="ts" setup>
const store = useSwarmStore();
const { customGroups } = storeToRefs(store);
</script>
<style scoped></style>
+6 -2
View File
@@ -31,7 +31,8 @@
</ul>
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
<octicon:container-24 class="inline-block" />
{{ $t("label.container", hostContainers[host.id]?.length ?? 0) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
@@ -107,7 +108,10 @@ useIntervalFn(
stat.totalCPU += container.stat.cpu;
stat.totalMem += container.stat.memoryUsage;
}
weightedStats[host].mostRecent = stat;
if (weightedStats[host]) {
// TODO fix this init
weightedStats[host].mostRecent = stat;
}
}
},
1000,
+1 -17
View File
@@ -1,20 +1,7 @@
<template>
<div class="flex items-center justify-end gap-4">
<slot name="more-items"></slot>
<Dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
<span
class="bg-red absolute top-0 right-px size-2 rounded-full"
v-if="hasUpdate && latestTag != latest?.tag"
></span>
</template>
<template #content>
<div class="w-72">
<Releases />
</div>
</template>
</Dropdown>
<Announcements />
<router-link
:to="{ name: '/settings' }"
@@ -58,7 +45,4 @@ async function logout() {
location.reload();
}
const { hasUpdate, latest } = useReleases();
const latestTag = useProfileStorage("releaseSeen", config.version);
</script>
@@ -2,80 +2,14 @@
<LogItem :logEntry>
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
</LogItem>
<div
class="alert alert-info my-4 w-auto flex-none font-sans text-[1rem] md:mx-auto md:w-1/2"
v-if="followEligible && showCard"
>
<carbon:information class="size-6 shrink-0 stroke-current" />
<div>
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
</div>
<div>
<TimedButton
v-if="automaticRedirect"
class="btn-primary btn-sm"
@finished="redirectNow()"
@click="showCard = false"
>
{{ $t("button.cancel") }}
</TimedButton>
<router-link
:to="{ name: '/container/[id]', params: { id: nextContainer.id } }"
class="btn btn-primary btn-sm"
v-else
>
{{ $t("button.redirect") }}
</router-link>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContainerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
const { logEntry } = defineProps<{
logEntry: ContainerEventLogEntry;
showContainerName?: boolean;
}>();
const showCard = ref(true);
const { containers } = useLoggingContext();
const store = useContainerStore();
const { containers: allContainers } = storeToRefs(store);
const nextContainer = computed(
() =>
[
...allContainers.value.filter(
(c) => c.created > containers.value[0].created && c.name === containers.value[0].name,
),
].sort((a, b) => +a.created - +b.created)[0],
);
const followEligible = computed(
() =>
router.currentRoute.value.name === "/container/[id]" && // we are on a container page
logEntry.event === "container-stopped" && // container was stopped
containers.value.length === 1 && // only one container
Date.now() - +logEntry.date < 5 * 60 * 1000 && // was stopped in the last 5 minutes
nextContainer.value !== undefined, // there is a next container
);
function redirectNow() {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.value?.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "/container/[id]", params: { id: nextContainer.value?.id } });
}
</script>
<style scoped>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="relative flex w-full items-start gap-x-2 group-[.compact]:items-stretch">
<LogStd :std="logEntry.std" class="shrink-0 select-none" v-if="showStd" />
<RandomColorTag class="shrink-0 select-none" :value="host.name" v-if="showHostname" />
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" />
<RandomColorTag class="shrink-0 select-none" :value="container.name" v-if="showContainerName" truncateRight />
<LogDate :date="logEntry.date" v-if="showTimestamp" class="shrink-0 select-none" />
<LogLevel
class="flex select-none"
@@ -97,7 +97,7 @@ const downloadParams = computed(() =>
const downloadUrl = computed(() =>
withBase(
`/api/containers/${containers.value.map((c) => c.host + ":" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
`/api/containers/${containers.value.map((c) => c.host + "~" + c.id).join(",")}/download?${new URLSearchParams(downloadParams.value).toString()}`,
),
);
</script>
@@ -1,7 +1,9 @@
<template>
<div class="tag grid w-40 overflow-hidden rounded-sm text-center text-sm text-white">
<div class="random-color col-start-1 row-start-1 brightness-75"></div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100 [direction:rtl]">{{ value }}</div>
<div class="col-start-1 row-start-1 truncate px-2 brightness-100" :class="truncateRight ? '[direction:rtl]' : ''">
{{ value }}
</div>
</div>
</template>
<script lang="ts">
@@ -29,8 +31,9 @@ const colors = [
] as const;
</script>
<script lang="ts" setup>
const { value } = defineProps<{
const { value, truncateRight = false } = defineProps<{
value: string;
truncateRight?: boolean;
}>();
const color = computed(() => colors[Math.abs(hashCode(value)) % colors.length]);
-74
View File
@@ -1,74 +0,0 @@
<template>
<ul class="space-y-4 p-2">
<li v-for="release in releases" v-if="releases?.length">
<div class="flex items-baseline gap-1">
<carbon:warning class="stroke-orange self-center" v-if="release.breaking > 0" />
<a :href="release.htmlUrl" class="link-primary text-lg font-bold" target="_blank" rel="noreferrer noopener">
{{ release.name }}
</a>
<span class="ml-1 text-xs"><distance-time :date="new Date(release.createdAt)" /></span>
<Tag class="bg-red ml-auto px-1 py-1 text-xs" v-if="release.tag === latest?.tag">
{{ $t("releases.latest") }}
</Tag>
</div>
<div class="text-base-content/80 text-sm">
{{ summary(release) }}
</div>
</li>
<li v-else>
<div class="text-base-content/80 text-sm">
{{ $t("releases.no_releases") }}
</div>
</li>
</ul>
</template>
<script setup lang="ts">
const { releases, latest } = useReleases();
const { t } = useI18n();
function summary(release: { features: number; bugFixes: number; breaking: number }) {
if (release.features > 0 && release.bugFixes > 0 && release.breaking > 0) {
return t("releases.three_parts", {
first: t("releases.breaking", { count: release.breaking }),
second: t("releases.features", { count: release.features }),
third: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.bugFixes > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.bugFixes", { count: release.bugFixes }),
});
}
if (release.features > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.features", { count: release.features }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.bugFixes > 0 && release.breaking > 0) {
return t("releases.two_parts", {
first: t("releases.bugFixes", { count: release.bugFixes }),
second: t("releases.breaking", { count: release.breaking }),
});
}
if (release.features > 0) {
return t("releases.features", { count: release.features });
}
if (release.bugFixes > 0) {
return t("releases.bugFixes", { count: release.bugFixes });
}
if (release.breaking > 0) {
return t("releases.breaking", { count: release.breaking });
}
}
</script>
<style scoped></style>
+9 -8
View File
@@ -1,10 +1,13 @@
<template>
<div v-if="ready" data-testid="side-menu" class="flex min-h-0 flex-col">
<Carousel v-model="selectedCard" class="flex-1">
<CarouselItem title="Hosts and Containers" id="host">
<CarouselItem :title="$t('label.host-menu')" id="host">
<HostMenu />
</CarouselItem>
<CarouselItem title="Services and Stacks" v-if="services.length > 0" id="swarm">
<CarouselItem :title="$t('label.group-menu')" v-if="customGroups.length > 0" id="group">
<GroupMenu />
</CarouselItem>
<CarouselItem :title="$t('label.swarm-menu')" v-if="services.length > 0" id="swarm">
<SwarmMenu />
</CarouselItem>
</Carousel>
@@ -20,16 +23,14 @@ const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore);
const route = useRoute();
const swarmStore = useSwarmStore();
const { services } = storeToRefs(swarmStore);
const selectedCard = ref<"host" | "swarm">("host");
const { services, customGroups } = storeToRefs(swarmStore);
const selectedCard = ref<"host" | "swarm" | "group">("host");
watch(
route,
() => {
if (route.meta.swarmMode) {
selectedCard.value = "swarm";
} else if (route.meta.containerMode) {
selectedCard.value = "host";
if (route.meta.menu && ["host", "swarm", "group"].includes(route.meta.menu as string)) {
selectedCard.value = route.meta.menu as "host" | "swarm" | "group";
}
},
{ immediate: true },
+1 -1
View File
@@ -11,7 +11,7 @@
{{ $t("label.container", stack.containers.length) }}
</Tag>
<Tag class="hidden font-mono max-md:hidden @3xl:block" size="small">
{{ $t("label.serivce", stack.services.length) }}
{{ $t("label.service", stack.services.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
+4 -28
View File
@@ -28,14 +28,14 @@
</details>
</li>
<li v-if="serivcesWithoutStacks.length > 0">
<li v-if="servicesWithoutStacks.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:circles-four />
{{ $t("label.services") }}
</summary>
<ul>
<li v-for="service in serivcesWithoutStacks" :key="service.name">
<li v-for="service in servicesWithoutStacks" :key="service.name">
<router-link :to="{ name: '/service/[name]', params: { name: service.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">
@@ -46,37 +46,13 @@
</ul>
</details>
</li>
<li v-if="customGroups.length > 0">
<details open>
<summary class="text-base-content/80 font-light">
<ph:bounding-box-fill />
{{ $t("label.custom-groups") }}
</summary>
<ul>
<li v-for="group in customGroups" :key="group.name">
<router-link :to="{ name: '/group/[name]', params: { name: group.name } }" active-class="menu-active">
<ph:stack-simple />
<div class="truncate">
{{ group.name }}
</div>
</router-link>
</li>
</ul>
</details>
</li>
</ul>
</template>
<script lang="ts" setup>
const store = useSwarmStore();
const { stacks, services, customGroups } = storeToRefs(store);
const { stacks, services } = storeToRefs(store);
const serivcesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
const servicesWithoutStacks = computed(() => services.value.filter((service) => !service.stack));
</script>
<style scoped>
.menu {
@apply text-[0.95rem];
}
</style>
+10
View File
@@ -46,6 +46,16 @@ const scrollToItem = (index: number) => {
};
const { pause, resume } = watchPausable(activeId, (v) => {
if (activeId.value) {
const index = cards.value?.map((c) => c.id).indexOf(activeId.value) ?? -1;
if (index !== -1) {
console.log("watching", activeId.value);
scrollToItem(index);
}
}
});
watchOnce(cards, () => {
if (activeId.value) {
const index = cards.value?.map((c) => c.id).indexOf(activeId.value) ?? -1;
if (index !== -1) {
+1
View File
@@ -2,6 +2,7 @@
<mdi:satellite-variant v-if="type == 'agent'" />
<ph:globe-simple v-else-if="type == 'remote'" />
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
<carbon:logo-kubernetes v-else-if="type == 'k8s'" />
<ph:computer-tower v-else />
</template>
<script setup lang="ts">
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="toast toast-end whitespace-normal max-md:m-0 max-md:w-full">
<div
class="alert max-w-xl max-md:rounded-none"
v-for="{ toast, options: { timed } } in toasts"
:key="toast.id"
:class="{
'alert-error': toast.type === 'error',
'alert-info': toast.type === 'info',
'alert-warning': toast.type === 'warning',
}"
>
<carbon:information class="size-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'warning'" />
<div>
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
<div v-html="toast.message" class="[&>a]:underline"></div>
</div>
<div>
<TimedButton
v-if="timed"
class="btn-primary btn-sm"
:duration="timed"
@finished="
removeToast(toast.id);
toast.action?.handler();
"
@cancelled="removeToast(toast.id)"
>
{{ toast.action?.label }}
</TimedButton>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)" v-else>
<mdi:close />
</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const { toasts, removeToast } = useToast();
</script>
+26 -7
View File
@@ -4,36 +4,55 @@ type Toast = {
title?: string;
message: string;
type: "success" | "error" | "warning" | "info";
action?: {
label: string;
handler: () => void;
};
};
type ToastOptions = {
expire?: number;
once?: boolean;
timed?: number;
};
const toasts = ref<Toast[]>([]);
const toasts = ref<
{
toast: Toast;
options: ToastOptions;
}[]
>([]);
const showToast = (
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
{ expire = -1, once = false, timed }: ToastOptions = { expire: -1, once: false },
) => {
if (once && toasts.value.some((t) => t.id === toast.id)) {
if (once && !toast.id) {
throw new Error("Toast id is required when once is true");
}
if (once && toasts.value.some((t) => t.toast.id === toast.id)) {
return;
}
toasts.value.push({
const toastWithId = {
id: Date.now().toString(),
createdAt: new Date(),
...toast,
createdAt: new Date(),
};
toasts.value.push({
toast: toastWithId,
options: { expire, once, timed },
});
if (expire > 0) {
setTimeout(() => {
removeToast(toasts.value[0].id);
removeToast(toastWithId.id);
}, expire);
}
};
const removeToast = (id: Toast["id"]) => {
toasts.value = toasts.value.filter((toast) => toast.id !== id);
toasts.value = toasts.value.filter((instance) => instance.toast.id !== id);
};
export const useToast = () => {
+1 -24
View File
@@ -52,29 +52,7 @@
<template #fallback> Loading dependencies... </template>
</Suspense>
</SideDrawer>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
v-for="toast in toasts"
:key="toast.id"
:class="{
'alert-error': toast.type === 'error',
'alert-info': toast.type === 'info',
'alert-warning': toast.type === 'warning',
}"
>
<carbon:information class="size-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
<carbon:warning class="size-6 shrink-0 stroke-current" v-else-if="toast.type === 'warning'" />
<div>
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
<div v-html="toast.message" class="[&>a]:underline"></div>
</div>
<div>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
</div>
</div>
</div>
<ToastModal />
</template>
<script lang="ts" setup>
@@ -86,7 +64,6 @@ import SideDrawer from "@/components/common/SideDrawer.vue";
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { toasts, removeToast } = useToast();
const drawer = useTemplateRef<InstanceType<typeof SideDrawer>>("drawer") as Ref<InstanceType<typeof SideDrawer>>;
const { component: drawerComponent, properties: drawerProperties, width: drawerWidth } = createDrawer(drawer);
+1 -1
View File
@@ -107,7 +107,7 @@
@utility menu-active {
&,
&:active {
--color-neutral: var(--color-primary);
--menu-active-bg: var(--color-primary);
}
}
+48 -3
View File
@@ -11,15 +11,18 @@
</template>
<script lang="ts" setup>
import { type Container } from "@/models/Container";
const route = useRoute("/container/[id]");
const id = toRef(() => route.params.id);
const containerStore = useContainerStore();
const currentContainer = containerStore.currentContainer(id);
const { ready } = storeToRefs(containerStore);
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { containers: allContainers } = storeToRefs(containerStore) as unknown as { containers: Ref<Container[]> };
const { showToast } = useToast();
const { t } = useI18n();
const router = useRouter();
watchEffect(() => {
if (ready.value) {
@@ -30,8 +33,50 @@ watchEffect(() => {
}
}
});
const redirectTrigger = ref(false);
watch(currentContainer, () => (redirectTrigger.value = false));
watchEffect(() => {
if (redirectTrigger.value) return;
if (!currentContainer.value) return;
if (currentContainer.value.state === "running") return;
if (Date.now() - +currentContainer.value.finishedAt > 5 * 60 * 1000) return;
const nextContainer = allContainers.value
.filter((c) => c.startedAt > currentContainer.value.startedAt && c.name === currentContainer.value.name)
.sort((a, b) => +a.created - +b.created)[0];
if (!nextContainer) return;
if (automaticRedirect.value) {
redirectTrigger.value = true;
showToast(
{
title: t("alert.similar-container-found.title"),
message: t("alert.similar-container-found.message", { containerId: nextContainer.id }),
type: "info",
action: {
label: t("button.cancel"),
handler: () => {
showToast(
{
title: t("alert.redirected.title"),
message: t("alert.redirected.message", { containerId: nextContainer.id }),
type: "info",
},
{ expire: 5000 },
);
router.push({ name: "/container/[id]", params: { id: nextContainer.id } });
},
},
},
{ timed: 4000 },
);
}
});
</script>
<route lang="yaml">
meta:
containerMode: true
menu: host
</route>
+1 -1
View File
@@ -22,5 +22,5 @@ watchEffect(() => {
</script>
<route lang="yaml">
meta:
swarmMode: true
menu: group
</route>
+1 -1
View File
@@ -26,5 +26,5 @@ watchEffect(() => {
</script>
<route lang="yaml">
meta:
containerMode: true
menu: host
</route>
+1 -1
View File
@@ -22,5 +22,5 @@ watchEffect(() => {
</script>
<route lang="yaml">
meta:
containerMode: true
menu: host
</route>
+1 -1
View File
@@ -28,5 +28,5 @@ watchEffect(() => {
</script>
<route lang="yaml">
meta:
swarmMode: true
menu: swarm
</route>
+3 -3
View File
@@ -8,8 +8,8 @@
<div>
<span v-html="$t('settings.using-version', { version: config.version })"></span>
<div
v-if="hasUpdate"
v-html="$t('settings.update-available', { nextVersion: latest?.name, href: latest?.htmlUrl })"
v-if="hasRelease"
v-html="$t('settings.update-available', { nextVersion: latestRelease?.name, href: latestRelease?.htmlUrl })"
></div>
</div>
</section>
@@ -154,7 +154,7 @@ import { availableLocales, i18n } from "@/modules/i18n";
const { t } = useI18n();
setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases();
const { latestRelease, hasRelease } = useAnnouncements();
const now = new Date();
const hoursAgo = (hours: number) => {
+1 -1
View File
@@ -28,5 +28,5 @@ watchEffect(() => {
</script>
<route lang="yaml">
meta:
swarmMode: true
menu: swarm
</route>
+37
View File
@@ -0,0 +1,37 @@
type Announcement = {
name: string;
announcement: boolean;
createdAt: Date;
body: string;
tag: string;
htmlUrl: string;
latest: boolean;
mentionsCount: number;
features: number;
bugFixes: number;
breaking: number;
};
const { data: releases } = useFetch(withBase("/api/releases")).get().json<Announcement[]>();
const otherAnnouncements = [] as Announcement[];
const announcements = computed(() => {
const newReleases =
releases.value?.map((release) => ({ ...release, createdAt: new Date(release.createdAt), announcement: false })) ??
[];
return [...newReleases, ...otherAnnouncements].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
});
const mostRecent = computed(() => announcements.value?.[0]);
const latestRelease = computed(() => announcements.value?.find((release) => release.latest && !release.announcement));
const hasRelease = computed(() => latestRelease.value !== undefined);
export function useAnnouncements() {
return {
mostRecent,
announcements,
latestRelease,
hasRelease,
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ export type Host = {
name: string;
nCPU: number;
memTotal: number;
type: "agent" | "local" | "remote" | "swarm";
type: "agent" | "local" | "remote" | "swarm" | "k8s";
endpoint: string;
available: boolean;
dockerVersion: string;
-29
View File
@@ -1,29 +0,0 @@
const { data: releases } = useFetch(withBase("/api/releases")).get().json<
{
name: string;
mentionsCount: number;
createdAt: string;
body: string;
tag: string;
htmlUrl: string;
latest: boolean;
features: number;
bugFixes: number;
breaking: number;
}[]
>();
const hasUpdate = computed(() => {
if (!releases.value?.length) return false;
return releases.value[0].tag !== config.version;
});
const latest = computed(() => releases.value?.find((release) => release.latest));
export function useReleases() {
return {
hasUpdate,
latest,
releases,
};
}
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -30,6 +30,7 @@ services:
- DOZZLE_FILTER=name=dozzle
- DOZZLE_NO_ANALYTICS=1
- DOZZLE_HOSTNAME=localhost
- DOZZLE_LEVEL=debug
ports:
- 7070:8080
build:
@@ -94,7 +95,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.49.1-jammy
image: mcr.microsoft.com/playwright:v1.50.1-jammy
working_dir: /app
volumes:
- .:/app
+1
View File
@@ -74,6 +74,7 @@ export default defineConfig({
{ text: "Healthcheck", link: "/guide/healthcheck" },
{ text: "Remote Hosts", link: "/guide/remote-hosts" },
{ text: "Swarm Mode", link: "/guide/swarm-mode" },
{ text: "K8s Mode", link: "/guide/k8s" },
{ text: "Supported Env Vars", link: "/guide/supported-env-vars" },
{ text: "Logging Files on Disk", link: "/guide/log-files-on-disk" },
{ text: "SQL Engine", link: "/guide/sql-engine" },
@@ -0,0 +1,57 @@
<template>
<div
class="banner fixed top-0 right-0 left-0 z-(--vp-z-index-layout-top) flex items-center overflow-hidden bg-[oklch(74%_0.16_232.661)] p-4 font-bold text-gray-800 dark:bg-[oklch(60%_0.126_221.723)] dark:text-white"
>
<div class="mx-auto flex items-center gap-2 lg:gap-4">
<span class="animate-bounce">🚀</span>K8s users, I need your help! I have added K8s support to Dozzle and I need
your feedback! 🙏
<a
href="https://github.com/amir20/dozzle/discussions/3614"
target="_blank"
class="btn btn-sm btn-primary text-white!"
>
See the discussion
</a>
</div>
<button class="btn btn-circle btn-ghost btn-xs ml-auto" @click="dismiss">
<svg class="swap-on fill-current" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512">
<polygon
points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49"
/>
</svg>
</button>
</div>
</template>
<script setup>
import { onMounted } from "vue";
const dismiss = () => {
localStorage.setItem("banner-dismissed", (Date.now() + 8.64e7 * 1).toString());
document.documentElement.classList.add("banner-dismissed");
};
onMounted(() => {
if (new Date(+localStorage.getItem("banner-dismissed")) > Date.now()) {
document.documentElement.classList.add("banner-dismissed");
}
});
</script>
<style>
.banner-dismissed {
--vp-layout-top-height: 0px !important;
}
@media (min-width: 375px) {
html {
--vp-layout-top-height: 100px;
}
}
@media (min-width: 768px) {
html {
--vp-layout-top-height: 63px;
}
}
</style>
<style scoped>
.banner-dismissed .banner {
display: none;
}
</style>
+3 -3
View File
@@ -1,14 +1,14 @@
<template>
<div class="stats mt-10">
<div class="stats stats-horizontal mt-10">
<div class="stat w-40 border-0!">
<div class="stat-title">Github Stars</div>
<div class="stat-title text-(--vp-c-text-1)">Github Stars</div>
<div class="stat-value">
<Counter :start="0" :end="data.stars" :duration="1000" :formatter="(value) => (value / 1e3).toFixed(1) + 'K'" />
</div>
</div>
<div class="stat">
<div class="stat-title">Docker Pulls</div>
<div class="stat-title text-(--vp-c-text-1)">Docker Pulls</div>
<div class="stat-value">
<Counter :start="0" :end="data.pulls" :duration="1000" :formatter="(value) => (value / 1e6).toFixed(0) + 'M'" />
</div>
+2
View File
@@ -7,6 +7,7 @@ import "./style.css";
import HeroVideo from "./components/HeroVideo.vue";
import BuyMeCoffee from "./components/BuyMeCoffee.vue";
import Stats from "./components/Stats.vue";
import Banner from "./components/Banner.vue";
export default {
...DefaultTheme,
@@ -15,6 +16,7 @@ export default {
"home-hero-image": () => h(HeroVideo),
"sidebar-nav-after": () => h(BuyMeCoffee),
"home-hero-actions-after": () => h(Stats),
"layout-top": () => h(Banner),
});
},
enhanceApp(ctx) {
+12 -1
View File
@@ -1,6 +1,17 @@
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
@plugin "daisyui" {
include: stat;
include: stat, button;
themes: light --default;
}
@plugin "daisyui/theme" {
name: "light";
default: true;
--depth: 0;
--noise: 0;
--radius-field: 2rem;
--color-primary: var(--vp-button-brand-bg);
}
/**
+1
View File
@@ -7,6 +7,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Banner: typeof import('./.vitepress/theme/components/Banner.vue')['default']
BuyMeCoffee: typeof import('./.vitepress/theme/components/BuyMeCoffee.vue')['default']
Counter: typeof import('./.vitepress/theme/components/Counter.vue')['default']
HeroVideo: typeof import('./.vitepress/theme/components/HeroVideo.vue')['default']
+61 -1
View File
@@ -47,9 +47,69 @@ networks:
driver: overlay
```
```yaml [k8s-dozzle.yml]
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
```
:::
See [swarm mode](/guide/swarm-mode) for more information on running Dozzle in Swarm.
See [swarm mode](/guide/swarm-mode) for more information on running Dozzle in Swarm and [Kubernetes](/guide/k8s) for running Dozzle in Kubernetes.
> [!TIP]
> If Docker Hub is blocked in your network, you can use the [GitHub Container Registry](https://ghcr.io/amir20/dozzle:latest) to pull the image. Use `ghcr.io/amir20/dozzle:latest` instead of `amir20/dozzle:latest`.
+140
View File
@@ -0,0 +1,140 @@
---
title: Kubernetes Support
---
# Kubernetes Support <Badge type="warning" text="beta" /> <Badge type="tip" text="v8.11.x" />
Dozzle now supports Kubernetes, allowing you to view logs from your Kubernetes pods. This feature is available in `v8.11` version of Dozzle.
## Kubernetes Setup
To set up Dozzle in Kubernetes, you can use the following YAML configuration using `DOZZLE_MODE=k8s`. This configuration includes a deployment and a service to expose Dozzle.
```yaml
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: dozzle-service
spec:
type: ClusterIP
selector:
app: dozzle
ports:
- port: 8080
targetPort: 8080
protocol: TCP
```
This configuration creates a service account, a cluster role, and a cluster role binding to allow Dozzle to access the necessary Kubernetes resources. It also creates a deployment for Dozzle and exposes it via a service.
All other features are supported as well, including authentication, filtering, and more. You can use the same environment variables as you would in Docker to configure Dozzle in Kubernetes.
> [!NOTE]
> Dozzle in Kubernetes is a new feature and may have some limitations compared to the Docker version. Please use this [discussion](https://github.com/amir20/dozzle/discussions/3614) to report any issues or suggestions for improvement.
## Metrics API
Dozzle relies on the [Kubernetes Metrics API](https://github.com/kubernetes-sigs/metrics-server) to retrieve resource usage information. The API can be installed using the following command:
```bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
```
To verify that the API is running, you can run the following command:
```bash
kubectl top pod
```
For now this is required to use Dozzle in Kubernetes.
## Default Namespace
By default, Dozzle will monitor all namespaces in the cluster. If you want to restrict Dozzle to a specific namespace, you can set the `DOZZLE_NAMESPACE` environment variable to the name of the namespace.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_NAMESPACE
value: "default"
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+18
View File
@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dozzle-ingress
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: dozzle.k3d.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dozzle-service
port:
number: 8080
+74
View File
@@ -0,0 +1,74 @@
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-viewer
---
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-viewer-role
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
# clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod-viewer-binding
subjects:
- kind: ServiceAccount
name: pod-viewer
namespace: default
roleRef:
kind: ClusterRole
name: pod-viewer-role
apiGroup: rbac.authorization.k8s.io
---
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dozzle
spec:
replicas: 1
selector:
matchLabels:
app: dozzle
template:
metadata:
labels:
app: dozzle
spec:
serviceAccountName: pod-viewer
containers:
- name: dozzle
image: amir20/dozzle:latest
imagePullPolicy: Never
ports:
- containerPort: 8080
env:
- name: DOZZLE_MODE
value: "k8s"
- name: DOZZLE_LEVEL
value: "debug"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: dozzle-service
spec:
type: ClusterIP
selector:
app: dozzle
ports:
- port: 8080
targetPort: 8080
protocol: TCP
+1 -1
View File
@@ -2,4 +2,4 @@ users:
amir:
email: ""
name: Amir
password: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
password: $2a$11$nXizfBMJrSwfo4ofkS62denLyCKX0X7VsmfTbMyD3thTkmuGNp8.m
+47 -17
View File
@@ -4,7 +4,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alexflint/go-arg v1.5.1
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/docker v27.4.1+incompatible
github.com/docker/docker v27.5.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
@@ -18,24 +18,28 @@ require (
github.com/spf13/afero v1.12.0
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/PuerkitoBio/goquery v1.10.1
github.com/go-chi/chi/v5 v5.2.0
github.com/PuerkitoBio/goquery v1.10.2
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/jwtauth/v5 v5.3.2
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/samber/lo v1.49.1
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.32.0
golang.org/x/sync v0.10.0
google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.36.2
golang.org/x/crypto v0.33.0
golang.org/x/sync v0.11.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.5
k8s.io/api v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
k8s.io/metrics v0.32.2
)
require (
@@ -45,14 +49,25 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
@@ -63,18 +78,33 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
go 1.23.4
go 1.24.0
+131 -79
View File
@@ -2,16 +2,14 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -25,34 +23,32 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A=
github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs=
github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -60,6 +56,16 @@ 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-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -67,18 +73,32 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.3.0/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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
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/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -95,8 +115,6 @@ github.com/lestrrat-go/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
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 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -111,8 +129,19 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/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/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -120,39 +149,49 @@ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2sz
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
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/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -168,9 +207,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCy
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
@@ -182,14 +222,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -205,15 +240,16 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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=
@@ -222,10 +258,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -237,27 +272,24 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -266,12 +298,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -281,36 +312,29 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
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.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI=
google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
@@ -319,3 +343,31 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/metrics v0.32.1 h1:Ou4nrEtZS2vFf7OJCf9z3+2kr0A00kQzfoSwxg0gXps=
k8s.io/metrics v0.32.1/go.mod h1:cLnai9XKYby1tNMX+xe8p9VLzTqrxYPcmqfCBoWObcM=
k8s.io/metrics v0.32.2 h1:7t/rZzTHFrGa9f94XcgLlm3ToAuJtdlHANcJEHlYl9g=
k8s.io/metrics v0.32.2/go.mod h1:VL3nJpzcgB6L5nSljkkzoE0nilZhVgcjCfNRgoylaIQ=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+49 -48
View File
@@ -13,7 +13,7 @@ import (
"encoding/json"
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/utils"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
@@ -89,7 +89,7 @@ func rpcErrToErr(err error) error {
}
}
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std docker.StdType) (<-chan *docker.LogEvent, error) {
func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (<-chan *container.LogEvent, error) {
stream, err := c.client.LogsBetweenDates(ctx, &pb.LogsBetweenDatesRequest{
ContainerId: containerID,
Since: timestamppb.New(since),
@@ -101,7 +101,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
return nil, err
}
events := make(chan *docker.LogEvent)
events := make(chan *container.LogEvent)
go func() {
sendLogs(stream, events)
@@ -111,7 +111,7 @@ func (c *Client) LogsBetweenDates(ctx context.Context, containerID string, since
return events, nil
}
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std docker.StdType, events chan<- *docker.LogEvent) error {
func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, since time.Time, std container.StdType, events chan<- *container.LogEvent) error {
stream, err := c.client.StreamLogs(ctx, &pb.StreamLogsRequest{
ContainerId: containerID,
Since: timestamppb.New(since),
@@ -125,7 +125,7 @@ func (c *Client) StreamContainerLogs(ctx context.Context, containerID string, si
return sendLogs(stream, events)
}
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.LogEvent) error {
func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *container.LogEvent) error {
for {
resp, err := stream.Recv()
if err != nil {
@@ -151,19 +151,19 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.Log
continue
}
events <- &docker.LogEvent{
events <- &container.LogEvent{
Id: resp.Event.Id,
ContainerID: resp.Event.ContainerId,
Message: message,
Timestamp: resp.Event.Timestamp.AsTime().Unix(),
Position: docker.LogPosition(resp.Event.Position),
Position: container.LogPosition(resp.Event.Position),
Level: resp.Event.Level,
Stream: resp.Event.Stream,
}
}
}
func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since time.Time, until time.Time, std docker.StdType) (io.ReadCloser, error) {
func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since time.Time, until time.Time, std container.StdType) (io.ReadCloser, error) {
out, err := c.client.StreamRawBytes(context.Background(), &pb.StreamRawBytesRequest{
ContainerId: containerID,
Since: timestamppb.New(since),
@@ -199,7 +199,7 @@ func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since t
return r, nil
}
func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerStat) error {
func (c *Client) StreamStats(ctx context.Context, stats chan<- container.ContainerStat) error {
stream, err := c.client.StreamStats(ctx, &pb.StreamStatsRequest{})
if err != nil {
return err
@@ -211,7 +211,7 @@ func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerS
return rpcErrToErr(err)
}
stats <- docker.ContainerStat{
stats <- container.ContainerStat{
CPUPercent: resp.Stat.CpuPercent,
MemoryPercent: resp.Stat.MemoryPercent,
MemoryUsage: resp.Stat.MemoryUsage,
@@ -220,7 +220,7 @@ func (c *Client) StreamStats(ctx context.Context, stats chan<- docker.ContainerS
}
}
func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.ContainerEvent) error {
func (c *Client) StreamEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
stream, err := c.client.StreamEvents(ctx, &pb.StreamEventsRequest{})
if err != nil {
return err
@@ -232,15 +232,16 @@ func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.Containe
return rpcErrToErr(err)
}
events <- docker.ContainerEvent{
events <- container.ContainerEvent{
ActorID: resp.Event.ActorId,
Name: resp.Event.Name,
Host: resp.Event.Host,
Time: resp.Event.Timestamp.AsTime(),
}
}
}
func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- docker.Container) error {
func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- container.Container) error {
stream, err := c.client.StreamContainerStarted(ctx, &pb.StreamContainerStartedRequest{})
if err != nil {
return err
@@ -252,7 +253,7 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
return rpcErrToErr(err)
}
containers <- docker.Container{
containers <- container.Container{
ID: resp.Container.Id,
Name: resp.Container.Name,
Image: resp.Container.Image,
@@ -270,16 +271,16 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
}
}
func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.Container, error) {
func (c *Client) FindContainer(ctx context.Context, containerID string) (container.Container, error) {
response, err := c.client.FindContainer(ctx, &pb.FindContainerRequest{ContainerId: containerID})
if err != nil {
return docker.Container{}, err
return container.Container{}, err
}
var stats []docker.ContainerStat
var stats []container.ContainerStat
for _, stat := range response.Container.Stats {
stats = append(stats, docker.ContainerStat{
stats = append(stats, container.ContainerStat{
ID: stat.Id,
CPUPercent: stat.CpuPercent,
MemoryPercent: stat.MemoryPercent,
@@ -287,7 +288,7 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.
})
}
return docker.Container{
return container.Container{
ID: response.Container.Id,
Name: response.Container.Name,
Image: response.Container.Image,
@@ -305,12 +306,12 @@ func (c *Client) FindContainer(ctx context.Context, containerID string) (docker.
}, nil
}
func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error) {
func (c *Client) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
in := &pb.ListContainersRequest{}
if filter != nil {
if labels != nil {
in.Filter = make(map[string]*pb.RepeatedString)
for k, v := range filter {
for k, v := range labels {
in.Filter[k] = &pb.RepeatedString{Values: v}
}
}
@@ -320,11 +321,11 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
return nil, err
}
containers := make([]docker.Container, 0)
for _, container := range response.Containers {
var stats []docker.ContainerStat
for _, stat := range container.Stats {
stats = append(stats, docker.ContainerStat{
containers := make([]container.Container, 0)
for _, c := range response.Containers {
var stats []container.ContainerStat
for _, stat := range c.Stats {
stats = append(stats, container.ContainerStat{
ID: stat.Id,
CPUPercent: stat.CpuPercent,
MemoryPercent: stat.MemoryPercent,
@@ -332,20 +333,20 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
})
}
containers = append(containers, docker.Container{
ID: container.Id,
Name: container.Name,
Image: container.Image,
Labels: container.Labels,
Group: container.Group,
Created: container.Created.AsTime(),
State: container.State,
Health: container.Health,
Host: container.Host,
Tty: container.Tty,
Command: container.Command,
StartedAt: container.Started.AsTime(),
FinishedAt: container.Finished.AsTime(),
containers = append(containers, container.Container{
ID: c.Id,
Name: c.Name,
Image: c.Image,
Labels: c.Labels,
Group: c.Group,
Created: c.Created.AsTime(),
State: c.State,
Health: c.Health,
Host: c.Host,
Tty: c.Tty,
Command: c.Command,
StartedAt: c.Started.AsTime(),
FinishedAt: c.Finished.AsTime(),
Stats: utils.RingBufferFrom(300, stats),
})
}
@@ -353,17 +354,17 @@ func (c *Client) ListContainers(ctx context.Context, filter docker.ContainerFilt
return containers, nil
}
func (c *Client) Host(ctx context.Context) (docker.Host, error) {
func (c *Client) Host(ctx context.Context) (container.Host, error) {
info, err := c.client.HostInfo(ctx, &pb.HostInfoRequest{})
if err != nil {
return docker.Host{
return container.Host{
Endpoint: c.endpoint,
Type: "agent",
Available: false,
}, err
}
return docker.Host{
return container.Host{
ID: info.Host.Id,
Name: info.Host.Name,
NCPU: int(info.Host.CpuCores),
@@ -375,16 +376,16 @@ func (c *Client) Host(ctx context.Context) (docker.Host, error) {
}, nil
}
func (c *Client) ContainerAction(ctx context.Context, containerId string, action docker.ContainerAction) error {
func (c *Client) ContainerAction(ctx context.Context, containerId string, action container.ContainerAction) error {
var containerAction pb.ContainerAction
switch action {
case docker.Start:
case container.Start:
containerAction = pb.ContainerAction_Start
case docker.Stop:
case container.Stop:
containerAction = pb.ContainerAction_Stop
case docker.Restart:
case container.Restart:
containerAction = pb.ContainerAction_Restart
}
+25 -25
View File
@@ -10,7 +10,7 @@ import (
"testing"
"time"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types/system"
"github.com/stretchr/testify/assert"
@@ -28,46 +28,46 @@ var client *MockedClient
type MockedClient struct {
mock.Mock
docker.Client
container.Client
}
func (m *MockedClient) FindContainer(ctx context.Context, id string) (docker.Container, error) {
func (m *MockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(docker.Container), args.Error(1)
return args.Get(0).(container.Container), args.Error(1)
}
func (m *MockedClient) ContainerActions(ctx context.Context, action docker.ContainerAction, containerID string) error {
func (m *MockedClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
args := m.Called(ctx, action, containerID)
return args.Error(0)
}
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- docker.ContainerEvent) error {
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
args := m.Called(ctx, events)
return args.Error(0)
}
func (m *MockedClient) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error) {
func (m *MockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]docker.Container), args.Error(1)
return args.Get(0).([]container.Container), args.Error(1)
}
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType docker.StdType) (io.ReadCloser, error) {
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
args := m.Called(ctx, id, since, stdType)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.ContainerStat) error {
func (m *MockedClient) ContainerStats(context.Context, string, chan<- container.ContainerStat) error {
return nil
}
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType docker.StdType) (io.ReadCloser, error) {
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
args := m.Called(ctx, id, from, to, stdType)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (m *MockedClient) Host() docker.Host {
func (m *MockedClient) Host() container.Host {
args := m.Called()
return args.Get(0).(docker.Host)
return args.Get(0).(container.Host)
}
func (m *MockedClient) IsSwarmMode() bool {
@@ -92,7 +92,7 @@ func init() {
}
client = &MockedClient{}
client.On("ListContainers", mock.Anything, mock.Anything).Return([]docker.Container{
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
{
ID: "123456",
Name: "test",
@@ -101,17 +101,17 @@ func init() {
},
}, nil)
client.On("Host").Return(docker.Host{
client.On("Host").Return(container.Host{
ID: "localhost",
Endpoint: "local",
Name: "local",
})
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
time.Sleep(5 * time.Second)
})
client.On("FindContainer", mock.Anything, "123456").Return(docker.Container{
client.On("FindContainer", mock.Anything, "123456").Return(container.Container{
ID: "123456",
Name: "test",
Host: "localhost",
@@ -124,13 +124,13 @@ func init() {
Labels: map[string]string{
"test": "test",
},
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
Stats: utils.NewRingBuffer[container.ContainerStat](300),
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
}, nil)
server, _ := NewServer(client, certs, "test", docker.ContainerFilter{})
server, _ := NewServer(client, certs, "test", container.ContainerLabels{})
go server.Serve(lis)
}
@@ -145,9 +145,9 @@ func TestFindContainer(t *testing.T) {
t.Fatal(err)
}
container, _ := rpc.FindContainer(context.Background(), "123456")
c, _ := rpc.FindContainer(context.Background(), "123456")
assert.Equal(t, container, docker.Container{
assert.Equal(t, c, container.Container{
ID: "123456",
Name: "test",
Host: "localhost",
@@ -160,7 +160,7 @@ func TestFindContainer(t *testing.T) {
Labels: map[string]string{
"test": "test",
},
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
Stats: utils.NewRingBuffer[container.ContainerStat](300),
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
@@ -173,9 +173,9 @@ func TestListContainers(t *testing.T) {
t.Fatal(err)
}
containers, _ := rpc.ListContainers(context.Background(), docker.ContainerFilter{})
containers, _ := rpc.ListContainers(context.Background(), container.ContainerLabels{})
assert.Equal(t, containers, []docker.Container{
assert.Equal(t, containers, []container.Container{
{
ID: "123456",
Name: "test",
@@ -189,7 +189,7 @@ func TestListContainers(t *testing.T) {
Labels: map[string]string{
"test": "test",
},
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
Stats: utils.NewRingBuffer[container.ContainerStat](300),
Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
StartedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
FinishedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
+67 -82
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v5.29.2
// protoc-gen-go v1.36.2
// protoc v5.29.3
// source: rpc.proto
package pb
@@ -22,11 +22,10 @@ const (
)
type ListContainersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
Filter map[string]*RepeatedString `protobuf:"bytes,1,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *ListContainersRequest) Reset() {
@@ -67,11 +66,10 @@ func (x *ListContainersRequest) GetFilter() map[string]*RepeatedString {
}
type RepeatedString struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RepeatedString) Reset() {
@@ -112,11 +110,10 @@ func (x *RepeatedString) GetValues() []string {
}
type ListContainersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
unknownFields protoimpl.UnknownFields
Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ListContainersResponse) Reset() {
@@ -157,12 +154,11 @@ func (x *ListContainersResponse) GetContainers() []*Container {
}
type FindContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Filter map[string]*RepeatedString `protobuf:"bytes,2,rep,name=filter,proto3" json:"filter,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *FindContainerRequest) Reset() {
@@ -210,11 +206,10 @@ func (x *FindContainerRequest) GetFilter() map[string]*RepeatedString {
}
type FindContainerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *FindContainerResponse) Reset() {
@@ -255,13 +250,12 @@ func (x *FindContainerResponse) GetContainer() *Container {
}
type StreamLogsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
StreamTypes int32 `protobuf:"varint,3,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamLogsRequest) Reset() {
@@ -316,11 +310,10 @@ func (x *StreamLogsRequest) GetStreamTypes() int32 {
}
type StreamLogsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
unknownFields protoimpl.UnknownFields
Event *LogEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamLogsResponse) Reset() {
@@ -361,14 +354,13 @@ func (x *StreamLogsResponse) GetEvent() *LogEvent {
}
type LogsBetweenDatesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogsBetweenDatesRequest) Reset() {
@@ -430,14 +422,13 @@ func (x *LogsBetweenDatesRequest) GetStreamTypes() int32 {
}
type StreamRawBytesRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Since *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=since,proto3" json:"since,omitempty"`
Until *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=until,proto3" json:"until,omitempty"`
StreamTypes int32 `protobuf:"varint,4,opt,name=streamTypes,proto3" json:"streamTypes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamRawBytesRequest) Reset() {
@@ -499,11 +490,10 @@ func (x *StreamRawBytesRequest) GetStreamTypes() int32 {
}
type StreamRawBytesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamRawBytesResponse) Reset() {
@@ -544,9 +534,9 @@ func (x *StreamRawBytesResponse) GetData() []byte {
}
type StreamEventsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamEventsRequest) Reset() {
@@ -580,11 +570,10 @@ func (*StreamEventsRequest) Descriptor() ([]byte, []int) {
}
type StreamEventsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
unknownFields protoimpl.UnknownFields
Event *ContainerEvent `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamEventsResponse) Reset() {
@@ -625,9 +614,9 @@ func (x *StreamEventsResponse) GetEvent() *ContainerEvent {
}
type StreamStatsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamStatsRequest) Reset() {
@@ -661,11 +650,10 @@ func (*StreamStatsRequest) Descriptor() ([]byte, []int) {
}
type StreamStatsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
unknownFields protoimpl.UnknownFields
Stat *ContainerStat `protobuf:"bytes,1,opt,name=stat,proto3" json:"stat,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamStatsResponse) Reset() {
@@ -706,9 +694,9 @@ func (x *StreamStatsResponse) GetStat() *ContainerStat {
}
type HostInfoRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HostInfoRequest) Reset() {
@@ -742,11 +730,10 @@ func (*HostInfoRequest) Descriptor() ([]byte, []int) {
}
type HostInfoResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
unknownFields protoimpl.UnknownFields
Host *Host `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *HostInfoResponse) Reset() {
@@ -787,9 +774,9 @@ func (x *HostInfoResponse) GetHost() *Host {
}
type StreamContainerStartedRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamContainerStartedRequest) Reset() {
@@ -823,11 +810,10 @@ func (*StreamContainerStartedRequest) Descriptor() ([]byte, []int) {
}
type StreamContainerStartedResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
unknownFields protoimpl.UnknownFields
Container *Container `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StreamContainerStartedResponse) Reset() {
@@ -868,12 +854,11 @@ func (x *StreamContainerStartedResponse) GetContainer() *Container {
}
type ContainerActionRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerActionRequest) Reset() {
@@ -921,9 +906,9 @@ func (x *ContainerActionRequest) GetAction() ContainerAction {
}
type ContainerActionResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerActionResponse) Reset() {
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2
// - protoc v5.29.3
// source: rpc.proto
package pb
+118 -112
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v5.29.2
// protoc-gen-go v1.36.2
// protoc v5.29.3
// source: types.proto
package pb
@@ -72,26 +72,25 @@ func (ContainerAction) EnumDescriptor() ([]byte, []int) {
}
type Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"` // deprecated
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
Started *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=started,proto3" json:"started,omitempty"`
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
Tty bool `protobuf:"varint,11,opt,name=tty,proto3" json:"tty,omitempty"`
Labels map[string]string `protobuf:"bytes,12,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Stats []*ContainerStat `protobuf:"bytes,13,rep,name=stats,proto3" json:"stats,omitempty"`
Group string `protobuf:"bytes,14,opt,name=group,proto3" json:"group,omitempty"`
Command string `protobuf:"bytes,15,opt,name=command,proto3" json:"command,omitempty"`
Finished *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=finished,proto3" json:"finished,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Container) Reset() {
@@ -237,14 +236,13 @@ func (x *Container) GetFinished() *timestamppb.Timestamp {
}
type ContainerStat struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
CpuPercent float64 `protobuf:"fixed64,2,opt,name=cpuPercent,proto3" json:"cpuPercent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,3,opt,name=memoryUsage,proto3" json:"memoryUsage,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,4,opt,name=memoryPercent,proto3" json:"memoryPercent,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerStat) Reset() {
@@ -306,17 +304,16 @@ func (x *ContainerStat) GetMemoryPercent() float64 {
}
type LogEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
unknownFields protoimpl.UnknownFields
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=containerId,proto3" json:"containerId,omitempty"`
Message *anypb.Any `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Level string `protobuf:"bytes,5,opt,name=level,proto3" json:"level,omitempty"`
Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"`
Position string `protobuf:"bytes,7,opt,name=position,proto3" json:"position,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogEvent) Reset() {
@@ -399,11 +396,10 @@ func (x *LogEvent) GetPosition() string {
}
type SimpleMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SimpleMessage) Reset() {
@@ -444,11 +440,10 @@ func (x *SimpleMessage) GetMessage() string {
}
type ComplexMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ComplexMessage) Reset() {
@@ -489,13 +484,13 @@ func (x *ComplexMessage) GetData() []byte {
}
type ContainerEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
ActorId string `protobuf:"bytes,1,opt,name=actorId,proto3" json:"actorId,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ContainerEvent) Reset() {
@@ -549,23 +544,29 @@ func (x *ContainerEvent) GetHost() string {
return ""
}
type Host struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
func (x *ContainerEvent) GetTimestamp() *timestamppb.Timestamp {
if x != nil {
return x.Timestamp
}
return nil
}
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
type Host struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
NodeAddress string `protobuf:"bytes,3,opt,name=nodeAddress,proto3" json:"nodeAddress,omitempty"`
Swarm bool `protobuf:"varint,4,opt,name=swarm,proto3" json:"swarm,omitempty"`
Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
OperatingSystem string `protobuf:"bytes,6,opt,name=operatingSystem,proto3" json:"operatingSystem,omitempty"`
OsVersion string `protobuf:"bytes,7,opt,name=osVersion,proto3" json:"osVersion,omitempty"`
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Host) Reset() {
@@ -755,44 +756,48 @@ var file_types_proto_rawDesc = []byte{
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x22, 0x24, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x52, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x6f,
0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x22, 0xaf, 0x03, 0x0a, 0x04, 0x48,
0x6f, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x6f,
0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x61,
0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d, 0x12,
0x32, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x2e,
0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62,
0x65, 0x6c, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67,
0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70,
0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a,
0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f,
0x73, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54,
0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x18,
0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x12,
0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52,
0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x33, 0x0a, 0x0f,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
0x09, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x74,
0x6f, 0x70, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x10,
0x02, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x8c, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x6f,
0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xaf, 0x03, 0x0a, 0x04, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d, 0x18, 0x04, 0x20,
0x01, 0x28, 0x08, 0x52, 0x05, 0x73, 0x77, 0x61, 0x72, 0x6d, 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61,
0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x28,
0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65,
0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69,
0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x6f, 0x73, 0x56, 0x65,
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x73, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65,
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a,
0x0a, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x08, 0x63, 0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65,
0x6d, 0x6f, 0x72, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72,
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x0b,
0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x2a, 0x33, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x74,
0x61, 0x72, 0x74, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x10, 0x01, 0x12,
0x0b, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x10, 0x02, 0x42, 0x13, 0x5a, 0x11,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70,
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -831,12 +836,13 @@ var file_types_proto_depIdxs = []int32{
10, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
11, // 5: protobuf.LogEvent.message:type_name -> google.protobuf.Any
10, // 6: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
9, // 7: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
10, // 7: protobuf.ContainerEvent.timestamp:type_name -> google.protobuf.Timestamp
9, // 8: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
9, // [9:9] is the sub-list for method output_type
9, // [9:9] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_types_proto_init() }
+35 -30
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
@@ -25,19 +26,20 @@ import (
)
type server struct {
client docker.Client
store *docker.ContainerStore
client container.Client
store *container.ContainerStore
version string
pb.UnimplementedAgentServiceServer
}
func newServer(client docker.Client, dozzleVersion string, filter docker.ContainerFilter) pb.AgentServiceServer {
func newServer(client container.Client, dozzleVersion string, labels container.ContainerLabels) pb.AgentServiceServer {
statsCollector := docker.NewDockerStatsCollector(client, labels)
return &server{
client: client,
version: dozzleVersion,
store: docker.NewContainerStore(context.Background(), client, filter),
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
@@ -47,17 +49,18 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
since = in.Since.AsTime()
}
container, err := s.store.FindContainer(in.ContainerId, docker.ContainerFilter{})
c, err := s.store.FindContainer(in.ContainerId, container.ContainerLabels{})
if err != nil {
return err
}
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, docker.StdType(in.StreamTypes))
reader, err := s.client.ContainerLogs(out.Context(), in.ContainerId, since, container.StdType(in.StreamTypes))
if err != nil {
return err
}
g := docker.NewEventGenerator(out.Context(), reader, container)
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(out.Context(), dockerReader, c)
for event := range g.Events {
out.Send(&pb.StreamLogsResponse{
@@ -74,17 +77,18 @@ func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_Stream
}
func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentService_LogsBetweenDatesServer) error {
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), docker.StdType(in.StreamTypes))
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
if err != nil {
return err
}
container, err := s.client.FindContainer(out.Context(), in.ContainerId)
c, err := s.client.FindContainer(out.Context(), in.ContainerId)
if err != nil {
return err
}
g := docker.NewEventGenerator(out.Context(), reader, container)
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(out.Context(), dockerReader, c)
for {
select {
@@ -101,7 +105,7 @@ func (s *server) LogsBetweenDates(in *pb.LogsBetweenDatesRequest, out pb.AgentSe
}
func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentService_StreamRawBytesServer) error {
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), docker.StdType(in.StreamTypes))
reader, err := s.client.ContainerLogsBetweenDates(out.Context(), in.ContainerId, in.Since.AsTime(), in.Until.AsTime(), container.StdType(in.StreamTypes))
if err != nil {
return err
@@ -129,7 +133,7 @@ func (s *server) StreamRawBytes(in *pb.StreamRawBytesRequest, out pb.AgentServic
}
func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_StreamEventsServer) error {
events := make(chan docker.ContainerEvent)
events := make(chan container.ContainerEvent)
s.store.SubscribeEvents(out.Context(), events)
@@ -138,9 +142,10 @@ func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_St
case event := <-events:
out.Send(&pb.StreamEventsResponse{
Event: &pb.ContainerEvent{
ActorId: event.ActorID,
Name: event.Name,
Host: event.Host,
ActorId: event.ActorID,
Name: event.Name,
Host: event.Host,
Timestamp: timestamppb.New(event.Time),
},
})
case <-out.Context().Done():
@@ -150,7 +155,7 @@ func (s *server) StreamEvents(in *pb.StreamEventsRequest, out pb.AgentService_St
}
func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_StreamStatsServer) error {
stats := make(chan docker.ContainerStat)
stats := make(chan container.ContainerStat)
s.store.SubscribeStats(out.Context(), stats)
@@ -172,14 +177,14 @@ func (s *server) StreamStats(in *pb.StreamStatsRequest, out pb.AgentService_Stre
}
func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest) (*pb.FindContainerResponse, error) {
filter := make(docker.ContainerFilter)
labels := make(container.ContainerLabels)
if in.GetFilter() != nil {
for k, v := range in.GetFilter() {
filter[k] = append(filter[k], v.GetValues()...)
labels[k] = append(labels[k], v.GetValues()...)
}
}
container, err := s.store.FindContainer(in.ContainerId, filter)
container, err := s.store.FindContainer(in.ContainerId, labels)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
@@ -204,14 +209,14 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
}
func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
filter := make(docker.ContainerFilter)
labels := make(container.ContainerLabels)
if in.GetFilter() != nil {
for k, v := range in.GetFilter() {
filter[k] = append(filter[k], v.GetValues()...)
labels[k] = append(labels[k], v.GetValues()...)
}
}
containers, err := s.store.ListContainers(filter)
containers, err := s.store.ListContainers(labels)
if err != nil {
return nil, err
}
@@ -267,7 +272,7 @@ func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.Host
}
func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, out pb.AgentService_StreamContainerStartedServer) error {
containers := make(chan docker.Container)
containers := make(chan container.Container)
go s.store.SubscribeNewContainers(out.Context(), containers)
@@ -297,16 +302,16 @@ func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, ou
}
func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequest) (*pb.ContainerActionResponse, error) {
var action docker.ContainerAction
var action container.ContainerAction
switch in.Action {
case pb.ContainerAction_Start:
action = docker.Start
action = container.Start
case pb.ContainerAction_Stop:
action = docker.Stop
action = container.Stop
case pb.ContainerAction_Restart:
action = docker.Restart
action = container.Restart
default:
return nil, status.Error(codes.InvalidArgument, "invalid action")
@@ -321,7 +326,7 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
return &pb.ContainerActionResponse{}, nil
}
func NewServer(client docker.Client, certificates tls.Certificate, dozzleVersion string, filter docker.ContainerFilter) (*grpc.Server, error) {
func NewServer(client container.Client, certificates tls.Certificate, dozzleVersion string, labels container.ContainerLabels) (*grpc.Server, error) {
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
if err != nil {
@@ -340,12 +345,12 @@ func NewServer(client docker.Client, certificates tls.Certificate, dozzleVersion
creds := credentials.NewTLS(tlsConfig)
grpcServer := grpc.NewServer(grpc.Creds(creds))
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, filter))
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion, labels))
return grpcServer, nil
}
func logEventToPb(event *docker.LogEvent) *pb.LogEvent {
func logEventToPb(event *container.LogEvent) *pb.LogEvent {
var message *anypb.Any
if event.Message == nil {
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/rs/zerolog/log"
)
@@ -42,7 +42,7 @@ func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader strin
func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(p.headerUser) != "" {
containerFilter, err := docker.ParseContainerFilter(r.Header.Get(p.headerFilter))
containerFilter, err := container.ParseContainerFilter(r.Header.Get(p.headerFilter))
if err != nil {
log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
}
+11 -11
View File
@@ -11,7 +11,7 @@ import (
"os"
"time"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/go-chi/jwtauth/v5"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
@@ -19,12 +19,12 @@ import (
)
type User struct {
Username string `json:"username" yaml:"-"`
Email string `json:"email" yaml:"email"`
Name string `json:"name" yaml:"name"`
Password string `json:"-" yaml:"password"`
Filter string `json:"-" yaml:"filter"`
ContainerFilter docker.ContainerFilter `json:"-" yaml:"-"`
Username string `json:"username" yaml:"-"`
Email string `json:"email" yaml:"email"`
Name string `json:"name" yaml:"name"`
Password string `json:"-" yaml:"password"`
Filter string `json:"-" yaml:"filter"`
ContainerLabels container.ContainerLabels `json:"-" yaml:"-"`
}
func (u User) AvatarURL() string {
@@ -35,12 +35,12 @@ func (u User) AvatarURL() string {
return fmt.Sprintf("https://gravatar.com/avatar/%s?d=https%%3A%%2F%%2Fui-avatars.com%%2Fapi%%2F/%s/128", hashEmail(u.Email), url.QueryEscape(name))
}
func newUser(username, email, name string, filter docker.ContainerFilter) User {
func newUser(username, email, name string, labels container.ContainerLabels) User {
return User{
Username: username,
Email: email,
Name: name,
ContainerFilter: filter,
ContainerLabels: labels,
}
}
@@ -197,9 +197,9 @@ func UserFromContext(ctx context.Context) *User {
}
email := claims["email"].(string)
name := claims["name"].(string)
containerFilter := docker.ContainerFilter{}
containerFilter := container.ContainerLabels{}
if filter, ok := claims["filter"].(string); ok {
containerFilter, err = docker.ParseContainerFilter(filter)
containerFilter, err = container.ParseContainerFilter(filter)
if err != nil {
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
}
+41
View File
@@ -0,0 +1,41 @@
package container
import (
"context"
"io"
"time"
)
type StdType int
const (
UNKNOWN StdType = 1 << iota
STDOUT
STDERR
)
const STDALL = STDOUT | STDERR
func (s StdType) String() string {
switch s {
case STDOUT:
return "stdout"
case STDERR:
return "stderr"
case STDALL:
return "all"
default:
return "unknown"
}
}
type Client interface {
ListContainers(context.Context, ContainerLabels) ([]Container, error)
FindContainer(context.Context, string) (Container, error)
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
ContainerEvents(context.Context, chan<- ContainerEvent) error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) error
Host() Host
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
}
@@ -1,4 +1,4 @@
package docker
package container
import (
"context"
@@ -13,32 +13,40 @@ import (
"golang.org/x/sync/semaphore"
)
type StatsCollector interface {
Start(parentCtx context.Context) bool
Subscribe(ctx context.Context, stats chan<- ContainerStat)
Stop()
}
type ContainerStore struct {
containers *xsync.MapOf[string, *Container]
subscribers *xsync.MapOf[context.Context, chan<- ContainerEvent]
newContainerSubscribers *xsync.MapOf[context.Context, chan<- Container]
client Client
statsCollector *StatsCollector
statsCollector StatsCollector
wg sync.WaitGroup
connected atomic.Bool
events chan ContainerEvent
ctx context.Context
filter ContainerFilter
labels ContainerLabels
}
func NewContainerStore(ctx context.Context, client Client, filter ContainerFilter) *ContainerStore {
log.Debug().Str("host", client.Host().Name).Interface("filter", filter).Msg("initializing container store")
const defaultTimeout = 10 * time.Second
func NewContainerStore(ctx context.Context, client Client, statsCollect StatsCollector, labels ContainerLabels) *ContainerStore {
log.Debug().Str("host", client.Host().Name).Interface("labels", labels).Msg("initializing container store")
s := &ContainerStore{
containers: xsync.NewMapOf[string, *Container](),
client: client,
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerEvent](),
newContainerSubscribers: xsync.NewMapOf[context.Context, chan<- Container](),
statsCollector: NewStatsCollector(client, filter),
statsCollector: statsCollect,
wg: sync.WaitGroup{},
events: make(chan ContainerEvent),
ctx: ctx,
filter: filter,
labels: labels,
}
s.wg.Add(1)
@@ -64,9 +72,9 @@ func (s *ContainerStore) checkConnectivity() error {
s.connected.Store(false)
}()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s is enough to fetch all containers
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if containers, err := s.client.ListContainers(ctx, s.filter); err != nil {
if containers, err := s.client.ListContainers(ctx, s.labels); err != nil {
return err
} else {
s.containers.Clear()
@@ -76,7 +84,7 @@ func (s *ContainerStore) checkConnectivity() error {
}
running := lo.Filter(containers, func(item Container, index int) bool {
return item.State != "exited"
return item.State != "exited" && !item.FullyLoaded
})
sem := semaphore.NewWeighted(maxFetchParallelism)
@@ -88,7 +96,7 @@ func (s *ContainerStore) checkConnectivity() error {
}
go func(c Container, i int) {
defer sem.Release(1)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 2s is hardcoded timeout for fetching container
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if container, err := s.client.FindContainer(ctx, c.ID); err == nil {
s.containers.Store(c.ID, &container)
@@ -107,38 +115,44 @@ func (s *ContainerStore) checkConnectivity() error {
return nil
}
func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, error) {
func (s *ContainerStore) ListContainers(labels ContainerLabels) ([]Container, error) {
s.wg.Wait()
if err := s.checkConnectivity(); err != nil {
return nil, err
}
validContainers, err := s.client.ListContainers(s.ctx, filter)
if err != nil {
return nil, err
}
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
return item.ID
})
containers := make([]Container, 0)
s.containers.Range(func(_ string, c *Container) bool {
if _, ok := validIDMap[c.ID]; ok {
containers = append(containers, *c)
if labels.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, labels)
if err != nil {
return nil, err
}
return true
})
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
return item.ID
})
s.containers.Range(func(_ string, c *Container) bool {
if _, ok := validIDMap[c.ID]; ok {
containers = append(containers, *c)
}
return true
})
} else {
s.containers.Range(func(_ string, c *Container) bool {
containers = append(containers, *c)
return true
})
}
return containers, nil
}
func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Container, error) {
func (s *ContainerStore) FindContainer(id string, labels ContainerLabels) (Container, error) {
s.wg.Wait()
if filter.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, filter)
if labels.Exists() {
validContainers, err := s.client.ListContainers(s.ctx, labels)
if err != nil {
return Container{}, err
}
@@ -154,31 +168,38 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
}
if container, ok := s.containers.Load(id); ok {
if container.StartedAt.IsZero() {
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
if !container.FullyLoaded {
log.Debug().Str("id", id).Msg("container is not fully loaded, fetching it")
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
return &newContainer, false
} else {
log.Error().Err(err).Msg("failed to fetch container")
return c, false
}
}
return c, false
}); ok {
event := ContainerEvent{
Name: "update",
Host: s.client.Host().ID,
ActorID: id,
}
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
select {
case events <- event:
case <-c.Done():
s.subscribers.Delete(c)
go func() {
event := ContainerEvent{
Name: "update",
Host: newContainer.Host,
ActorID: id,
Container: newContainer,
}
return true
})
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
select {
case events <- event:
case <-c.Done():
s.subscribers.Delete(c)
}
return true
})
}()
return *newContainer, nil
}
}
@@ -234,13 +255,37 @@ func (s *ContainerStore) init() {
for {
select {
case event := <-s.events:
log.Trace().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
log.Debug().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
switch event.Name {
case "start":
case "create":
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx, s.filter)
list, _ := s.client.ListContainers(ctx, s.labels)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
return item.ID == container.ID
})
if valid {
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
case containers <- container:
case <-c.Done():
}
return true
})
}
}
cancel()
case "start":
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx, s.labels)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
@@ -248,7 +293,6 @@ func (s *ContainerStore) init() {
})
if valid {
log.Debug().Str("id", container.ID).Msg("container started")
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
@@ -264,6 +308,42 @@ func (s *ContainerStore) init() {
log.Debug().Str("id", event.ActorID).Msg("container destroyed")
s.containers.Delete(event.ActorID)
case "update":
started := false
updatedContainer, _ := s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
newContainer := event.Container
if newContainer.State == "running" && c.State != "running" {
started = true
}
c.Name = newContainer.Name
c.State = newContainer.State
c.Labels = newContainer.Labels
c.StartedAt = newContainer.StartedAt
c.FinishedAt = newContainer.FinishedAt
c.Created = newContainer.Created
c.Host = newContainer.Host
return c, false
} else {
return c, true
}
})
if started {
s.subscribers.Range(func(ctx context.Context, events chan<- ContainerEvent) bool {
select {
case events <- ContainerEvent{
Name: "start",
ActorID: updatedContainer.ID,
Host: updatedContainer.Host,
}:
case <-ctx.Done():
s.subscribers.Delete(ctx)
}
return true
})
}
case "die":
s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
@@ -1,4 +1,4 @@
package docker
package container
import (
"context"
@@ -14,7 +14,7 @@ type mockedClient struct {
Client
}
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
func (m *mockedClient) ListContainers(ctx context.Context, filter ContainerLabels) ([]Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]Container), args.Error(1)
}
@@ -48,7 +48,7 @@ func TestContainerStore_List(t *testing.T) {
Name: "test",
},
}, nil)
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
ctx := args.Get(0).(context.Context)
<-ctx.Done()
})
@@ -63,11 +63,9 @@ func TestContainerStore_List(t *testing.T) {
Stats: utils.NewRingBuffer[ContainerStat](300),
}, nil)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := NewContainerStore(ctx, client, ContainerFilter{})
containers, _ := store.ListContainers(ContainerFilter{})
collector := &fakeStatsCollector{}
store := NewContainerStore(t.Context(), client, collector, ContainerLabels{})
containers, _ := store.ListContainers(ContainerLabels{})
assert.Equal(t, containers[0].ID, "1234")
}
@@ -83,7 +81,7 @@ func TestContainerStore_die(t *testing.T) {
},
}, nil)
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).Return(nil).
Run(func(args mock.Arguments) {
ctx := args.Get(0).(context.Context)
events := args.Get(1).(chan<- ContainerEvent)
@@ -98,7 +96,7 @@ func TestContainerStore_die(t *testing.T) {
ID: "localhost",
})
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- docker.ContainerStat")).Return(nil)
client.On("ContainerStats", mock.Anything, "1234", mock.AnythingOfType("chan<- container.ContainerStat")).Return(nil)
client.On("FindContainer", mock.Anything, "1234").Return(Container{
ID: "1234",
@@ -107,15 +105,19 @@ func TestContainerStore_die(t *testing.T) {
Stats: utils.NewRingBuffer[ContainerStat](300),
}, nil)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := NewContainerStore(ctx, client, ContainerFilter{})
store := NewContainerStore(t.Context(), client, &fakeStatsCollector{}, ContainerLabels{})
// Wait until we get the event
events := make(chan ContainerEvent)
store.SubscribeEvents(ctx, events)
store.SubscribeEvents(t.Context(), events)
<-events
containers, _ := store.ListContainers(ContainerFilter{})
containers, _ := store.ListContainers(ContainerLabels{})
assert.Equal(t, containers[0].State, "exited")
}
type fakeStatsCollector struct{}
func (f *fakeStatsCollector) Subscribe(_ context.Context, _ chan<- ContainerStat) {}
func (f *fakeStatsCollector) Start(_ context.Context) bool { return true }
func (f *fakeStatsCollector) Stop() {}
@@ -1,4 +1,4 @@
package docker
package container
import (
"html"
@@ -30,7 +30,6 @@ func escape(logEvent *LogEvent) {
}
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
switch value := pair.Value.(type) {
case string:
@@ -1,14 +1,10 @@
package docker
package container
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"hash/fnv"
"io"
"strings"
"sync"
"time"
@@ -23,30 +19,26 @@ import (
type EventGenerator struct {
Events chan *LogEvent
Errors chan error
reader *bufio.Reader
reader LogReader
next *LogEvent
buffer chan *LogEvent
tty bool
wg sync.WaitGroup
containerID string
ctx context.Context
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var ErrBadHeader = fmt.Errorf("dozzle/docker: unable to read header")
func NewEventGenerator(ctx context.Context, reader io.Reader, container Container) *EventGenerator {
type LogReader interface {
Read() (string, StdType, error)
}
func NewEventGenerator(ctx context.Context, reader LogReader, container Container) *EventGenerator {
generator := &EventGenerator{
reader: bufio.NewReader(reader),
reader: reader,
buffer: make(chan *LogEvent, 100),
Errors: make(chan error, 1),
Events: make(chan *LogEvent),
tty: container.Tty,
containerID: container.ID,
ctx: ctx,
}
@@ -90,7 +82,7 @@ loop:
func (g *EventGenerator) consumeReader() {
for {
message, streamType, readerError := readEvent(g.reader, g.tty)
message, streamType, readerError := g.reader.Read()
if message != "" {
logEvent := createEvent(message, streamType)
logEvent.ContainerID = g.containerID
@@ -123,50 +115,6 @@ func (g *EventGenerator) peek() *LogEvent {
}
}
func readEvent(reader *bufio.Reader, tty bool) (string, StdType, error) {
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
buffer := bufPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufPool.Put(buffer)
var streamType StdType = STDOUT
if tty {
message, err := reader.ReadString('\n')
if err != nil {
return message, streamType, err
}
return message, streamType, nil
} else {
n, err := io.ReadFull(reader, header)
if err != nil {
return "", streamType, err
}
if n != 8 {
log.Warn().Bytes("header", header).Msg("short read")
message, _ := reader.ReadString('\n')
return message, streamType, ErrBadHeader
}
switch header[0] {
case 1:
streamType = STDOUT
case 2:
streamType = STDERR
default:
log.Warn().Bytes("header", header).Msg("unknown stream type")
}
count := binary.BigEndian.Uint32(header[4:])
if count == 0 {
return "", streamType, nil
}
_, err = io.CopyN(buffer, reader, int64(count))
if err != nil {
return "", streamType, err
}
return buffer.String(), streamType, nil
}
}
func createEvent(message string, streamType StdType) *LogEvent {
h := fnv.New32a()
h.Write([]byte(message))
@@ -1,12 +1,9 @@
package docker
package container
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"io"
"reflect"
"strings"
"sync"
"testing"
"time"
@@ -18,9 +15,8 @@ import (
func TestEventGenerator_Events_tty(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
g := NewEventGenerator(context.Background(), reader, Container{Tty: true})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: true})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
@@ -29,9 +25,8 @@ func TestEventGenerator_Events_tty(t *testing.T) {
func TestEventGenerator_Events_non_tty(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
event := <-g.Events
require.NotNil(t, event, "Expected event to not be nil, but got nil")
@@ -40,9 +35,8 @@ func TestEventGenerator_Events_non_tty(t *testing.T) {
func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
<-g.Events
_, ok := <-g.Events
@@ -51,20 +45,31 @@ func TestEventGenerator_Events_non_tty_close_channel(t *testing.T) {
func TestEventGenerator_Events_routines_done(t *testing.T) {
input := "example input"
reader := bytes.NewReader(makeMessage(input, STDOUT))
g := NewEventGenerator(context.Background(), reader, Container{Tty: false})
g := NewEventGenerator(context.Background(), makeFakeReader(input, STDOUT), Container{Tty: false})
<-g.Events
assert.False(t, waitTimeout(&g.wg, 1*time.Second), "Expected routines to be done")
}
func makeMessage(message string, stream StdType) []byte {
data := make([]byte, 8)
binary.BigEndian.PutUint32(data[4:], uint32(len(message)))
data[0] = byte(stream / 2)
data = append(data, []byte(message)...)
type mockLogReader struct {
messages []string
types []StdType
i int
}
return data
func (m *mockLogReader) Read() (string, StdType, error) {
if m.i >= len(m.messages) {
return "", 0, io.EOF
}
m.i++
return m.messages[m.i-1], m.types[m.i-1], nil
}
func makeFakeReader(message string, stream StdType) LogReader {
return &mockLogReader{
messages: []string{message},
types: []StdType{stream},
}
}
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
@@ -156,23 +161,3 @@ func Test_createEvent(t *testing.T) {
})
}
}
type mockReadCloser struct {
bytes []byte
}
func (m mockReadCloser) Read(p []byte) (int, error) {
return copy(p, m.bytes), nil
}
func Benchmark_readEvent(b *testing.B) {
b.ReportAllocs()
data := makeMessage("2020-05-13T18:55:37.772853839Z {\"key\": \"value\"}\n", STDOUT)
reader := bufio.NewReader(mockReadCloser{bytes: data})
for i := 0; i < b.N; i++ {
readEvent(reader, true)
}
}
@@ -1,4 +1,4 @@
package docker
package container
import (
"fmt"
@@ -25,6 +25,7 @@ type Host struct {
AgentVersion string `json:"agentVersion,omitempty"`
Type string `json:"type"`
Available bool `json:"available"`
Swarm bool `json:"-"`
}
func (h Host) String() string {
@@ -1,4 +1,4 @@
package docker
package container
import (
"regexp"
@@ -1,4 +1,4 @@
package docker
package container
import (
"encoding/json"
@@ -1,4 +1,4 @@
package docker
package container
import (
"errors"
@@ -1,4 +1,4 @@
package docker
package container
import (
"encoding/json"
@@ -1,4 +1,4 @@
package docker
package container
import (
"regexp"
@@ -1,4 +1,4 @@
package docker
package container
import (
"fmt"
@@ -7,25 +7,25 @@ import (
"time"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types/filters"
)
// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
Group string `json:"group,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Tty bool `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
Group string `json:"group,omitempty"`
FullyLoaded bool `json:"-,omitempty"`
}
// ContainerStat represent stats instant for a container
@@ -43,12 +43,13 @@ type ContainerEvent struct {
ActorID string `json:"actorId"`
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
Time time.Time `json:"time"`
Container *Container `json:"-"`
}
type ContainerFilter map[string][]string
type ContainerLabels map[string][]string
func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
filter := make(ContainerFilter)
func ParseContainerFilter(commaValues string) (ContainerLabels, error) {
filter := make(ContainerLabels)
if commaValues == "" {
return filter, nil
}
@@ -66,21 +67,10 @@ func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
return filter, nil
}
func (f ContainerFilter) Exists() bool {
func (f ContainerLabels) Exists() bool {
return len(f) > 0
}
func (f ContainerFilter) asArgs() filters.Args {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
return filterArgs
}
type LogPosition string
const (
+95 -130
View File
@@ -11,112 +11,76 @@ import (
"encoding/json"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
type StdType int
const (
UNKNOWN StdType = 1 << iota
STDOUT
STDERR
)
const STDALL = STDOUT | STDERR
func (s StdType) String() string {
switch s {
case STDOUT:
return "stdout"
case STDERR:
return "stderr"
case STDALL:
return "all"
default:
return "unknown"
}
}
type DockerCLI interface {
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, container.LogsOptions) (io.ReadCloser, error)
ContainerList(context.Context, docker.ListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, docker.LogsOptions) (io.ReadCloser, error)
Events(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error)
Ping(ctx context.Context) (types.Ping, error)
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error
ContainerStop(ctx context.Context, containerID string, options docker.StopOptions) error
ContainerRestart(ctx context.Context, containerID string, options docker.StopOptions) error
Info(ctx context.Context) (system.Info, error)
}
type Client interface {
ListContainers(context.Context, ContainerFilter) ([]Container, error)
FindContainer(context.Context, string) (Container, error)
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
ContainerEvents(context.Context, chan<- ContainerEvent) error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() Host
ContainerActions(ctx context.Context, action ContainerAction, containerID string) error
IsSwarmMode() bool
SystemInfo() system.Info
}
type httpClient struct {
type DockerClient struct {
cli DockerCLI
host Host
host container.Host
info system.Info
}
func NewClient(cli DockerCLI, host Host) Client {
func NewClient(cli DockerCLI, host container.Host) *DockerClient {
info, err := cli.Info(context.Background())
if err != nil {
log.Error().Err(err).Msg("Failed to get docker info")
}
host.NCPU = info.NCPU
host.MemTotal = info.MemTotal
host.DockerVersion = info.ServerVersion
return &httpClient{
cli: cli,
host: host,
info: info,
}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewLocalClient(hostname string) (Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}
info, err := cli.Info(context.Background())
if err != nil {
return nil, err
}
id := info.ID
if info.Swarm.NodeID != "" {
id = info.Swarm.NodeID
}
host := Host{
ID: id,
host.ID = id
host.NCPU = info.NCPU
host.MemTotal = info.MemTotal
host.DockerVersion = info.ServerVersion
host.Swarm = info.Swarm.NodeID != ""
return &DockerClient{
cli: cli,
host: host,
info: info,
}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewLocalClient(hostname string) (*DockerClient, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithUserAgent("Docker-Client/Dozzle"))
if err != nil {
return nil, err
}
info, err := cli.Info(context.Background())
if err != nil {
return nil, err
}
host := container.Host{
Name: info.Name,
MemTotal: info.MemTotal,
NCPU: info.NCPU,
Endpoint: "local",
Type: "local",
}
@@ -128,7 +92,7 @@ func NewLocalClient(hostname string) (Client, error) {
return NewClient(cli, host), nil
}
func NewRemoteClient(host Host) (Client, error) {
func NewRemoteClient(host container.Host) (*DockerClient, error) {
if host.URL.Scheme != "tcp" {
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
}
@@ -144,7 +108,7 @@ func NewRemoteClient(host Host) (Client, error) {
log.Debug().Msg("Not using TLS for remote client")
}
opts = append(opts, client.WithAPIVersionNegotiation())
opts = append(opts, client.WithAPIVersionNegotiation(), client.WithUserAgent("Docker-Client/Dozzle"))
cli, err := client.NewClientWithOpts(opts...)
@@ -158,33 +122,39 @@ func NewRemoteClient(host Host) (Client, error) {
}
// Finds a container by id, skipping the filters
func (d *httpClient) FindContainer(ctx context.Context, id string) (Container, error) {
func (d *DockerClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
log.Debug().Str("id", id).Msg("Finding container")
if json, err := d.cli.ContainerInspect(ctx, id); err == nil {
return newContainerFromJSON(json, d.host.ID), nil
} else {
return Container{}, err
return container.Container{}, err
}
}
func (d *httpClient) ContainerActions(ctx context.Context, action ContainerAction, containerID string) error {
func (d *DockerClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
switch action {
case Start:
return d.cli.ContainerStart(ctx, containerID, container.StartOptions{})
case Stop:
return d.cli.ContainerStop(ctx, containerID, container.StopOptions{})
case Restart:
return d.cli.ContainerRestart(ctx, containerID, container.StopOptions{})
case container.Start:
return d.cli.ContainerStart(ctx, containerID, docker.StartOptions{})
case container.Stop:
return d.cli.ContainerStop(ctx, containerID, docker.StopOptions{})
case container.Restart:
return d.cli.ContainerRestart(ctx, containerID, docker.StopOptions{})
default:
return fmt.Errorf("unknown action: %s", action)
}
}
func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
log.Debug().Interface("filter", filter).Str("host", d.host.Name).Msg("Listing containers")
containerListOptions := container.ListOptions{
Filters: filter.asArgs(),
func (d *DockerClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
log.Debug().Interface("labels", labels).Str("host", d.host.Name).Msg("Listing containers")
filterArgs := filters.NewArgs()
for key, values := range labels {
for _, value := range values {
filterArgs.Add(key, value)
}
}
containerListOptions := docker.ListOptions{
Filters: filterArgs,
All: true,
}
list, err := d.cli.ContainerList(ctx, containerListOptions)
@@ -192,7 +162,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter)
return nil, err
}
var containers = make([]Container, 0, len(list))
var containers = make([]container.Container, 0, len(list))
for _, c := range list {
containers = append(containers, newContainer(c, d.host.ID))
}
@@ -204,7 +174,7 @@ func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter)
return containers, nil
}
func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<- ContainerStat) error {
func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
response, err := d.cli.ContainerStats(ctx, id, true)
if err != nil {
@@ -213,7 +183,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
var v *container.StatsResponse
var v *docker.StatsResponse
for {
if err := decoder.Decode(&v); err != nil {
return err
@@ -243,7 +213,7 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
select {
case <-ctx.Done():
return nil
case stats <- ContainerStat{
case stats <- container.ContainerStat{
ID: id,
CPUPercent: cpuPercent,
MemoryPercent: memPercent,
@@ -254,13 +224,13 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
}
}
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType StdType) (io.ReadCloser, error) {
func (d *DockerClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
log.Debug().Str("id", id).Time("since", since).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Streaming logs for container")
sinceQuery := since.Add(-50 * time.Millisecond).Format(time.RFC3339Nano)
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
options := docker.LogsOptions{
ShowStdout: stdType&container.STDOUT != 0,
ShowStderr: stdType&container.STDERR != 0,
Follow: true,
Tail: strconv.Itoa(100),
Timestamps: true,
@@ -275,7 +245,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Ti
return reader, nil
}
func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- ContainerEvent) error {
func (d *DockerClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
dockerMessages, err := d.cli.Events(ctx, events.ListOptions{})
for {
@@ -287,22 +257,23 @@ func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- Contai
case message := <-dockerMessages:
if message.Type == events.ContainerEventType && len(message.Actor.ID) > 0 {
messages <- ContainerEvent{
messages <- container.ContainerEvent{
ActorID: message.Actor.ID[:12],
Name: string(message.Action),
Host: d.host.ID,
ActorAttributes: message.Actor.Attributes,
Time: time.Now(),
}
}
}
}
}
func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
func (d *DockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
log.Debug().Str("id", id).Time("from", from).Time("to", to).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Fetching logs between dates for container")
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
options := docker.LogsOptions{
ShowStdout: stdType&container.STDOUT != 0,
ShowStderr: stdType&container.STDERR != 0,
Timestamps: true,
Since: from.Format(time.RFC3339Nano),
Until: to.Format(time.RFC3339Nano),
@@ -316,24 +287,17 @@ func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, f
return reader, nil
}
func (d *httpClient) Ping(ctx context.Context) (types.Ping, error) {
return d.cli.Ping(ctx)
func (d *DockerClient) Ping(ctx context.Context) error {
_, err := d.cli.Ping(ctx)
return err
}
func (d *httpClient) Host() Host {
func (d *DockerClient) Host() container.Host {
log.Debug().Str("host", d.host.Name).Msg("Fetching host")
return d.host
}
func (d *httpClient) IsSwarmMode() bool {
return d.info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive
}
func (d *httpClient) SystemInfo() system.Info {
return d.info
}
func newContainer(c types.Container, host string) Container {
func newContainer(c types.Container, host string) container.Container {
name := "no name"
if c.Labels["dev.dozzle.name"] != "" {
name = c.Labels["dev.dozzle.name"]
@@ -345,7 +309,7 @@ func newContainer(c types.Container, host string) Container {
if c.Labels["dev.dozzle.group"] != "" {
group = c.Labels["dev.dozzle.group"]
}
return Container{
return container.Container{
ID: c.ID[:12],
Name: name,
Image: c.Image,
@@ -354,12 +318,12 @@ func newContainer(c types.Container, host string) Container {
State: c.State,
Host: host,
Labels: c.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
Group: group,
}
}
func newContainerFromJSON(c types.ContainerJSON, host string) Container {
func newContainerFromJSON(c types.ContainerJSON, host string) container.Container {
name := "no name"
if c.Config.Labels["dev.dozzle.name"] != "" {
name = c.Config.Labels["dev.dozzle.name"]
@@ -372,17 +336,18 @@ func newContainerFromJSON(c types.ContainerJSON, host string) Container {
group = c.Config.Labels["dev.dozzle.group"]
}
container := Container{
ID: c.ID[:12],
Name: name,
Image: c.Config.Image,
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
State: c.State.Status,
Host: host,
Labels: c.Config.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
Group: group,
Tty: c.Config.Tty,
container := container.Container{
ID: c.ID[:12],
Name: name,
Image: c.Config.Image,
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
State: c.State.Status,
Host: host,
Labels: c.Config.Labels,
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
Group: group,
Tty: c.Config.Tty,
FullyLoaded: true,
}
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
+31 -30
View File
@@ -10,8 +10,9 @@ import (
"testing"
"github.com/amir20/dozzle/internal/container"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -23,7 +24,7 @@ type mockedProxy struct {
DockerCLI
}
func (m *mockedProxy) ContainerList(context.Context, container.ListOptions) ([]types.Container, error) {
func (m *mockedProxy) ContainerList(context.Context, docker.ListOptions) ([]types.Container, error) {
args := m.Called()
containers, ok := args.Get(0).([]types.Container)
if !ok && args.Get(0) != nil {
@@ -33,7 +34,7 @@ func (m *mockedProxy) ContainerList(context.Context, container.ListOptions) ([]t
}
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options container.LogsOptions) (io.ReadCloser, error) {
func (m *mockedProxy) ContainerLogs(ctx context.Context, id string, options docker.LogsOptions) (io.ReadCloser, error) {
args := m.Called(ctx, id, options)
reader, ok := args.Get(0).(io.ReadCloser)
if !ok && args.Get(0) != nil {
@@ -47,11 +48,11 @@ func (m *mockedProxy) ContainerInspect(ctx context.Context, containerID string)
return args.Get(0).(types.ContainerJSON), args.Error(1)
}
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, error) {
return container.StatsResponseReader{}, nil
func (m *mockedProxy) ContainerStats(ctx context.Context, containerID string, stream bool) (docker.StatsResponseReader, error) {
return docker.StatsResponseReader{}, nil
}
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error {
args := m.Called(ctx, containerID, options)
err := args.Get(0)
@@ -63,7 +64,7 @@ func (m *mockedProxy) ContainerStart(ctx context.Context, containerID string, op
return nil
}
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, options docker.StopOptions) error {
args := m.Called(ctx, containerID, options)
err := args.Get(0)
@@ -74,7 +75,7 @@ func (m *mockedProxy) ContainerStop(ctx context.Context, containerID string, opt
return nil
}
func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, options docker.StopOptions) error {
args := m.Called(ctx, containerID, options)
err := args.Get(0)
@@ -89,9 +90,9 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string,
func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.")
@@ -101,9 +102,9 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.")
@@ -124,9 +125,9 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
list, err := client.ListContainers(context.Background(), ContainerFilter{})
list, err := client.ListContainers(context.Background(), container.ContainerLabels{})
require.NoError(t, err, "error should not return an error.")
Ids := []string{"1234567890_a", "abcdefghijkl"}
@@ -149,7 +150,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
reader := io.NopCloser(bytes.NewReader(b))
since := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
options := container.LogsOptions{
options := docker.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
@@ -158,8 +159,8 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
Since: "2020-12-31T23:59:59.95Z"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
logReader, _ := client.ContainerLogs(context.Background(), id, since, STDALL)
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
logReader, _ := client.ContainerLogs(context.Background(), id, since, container.STDALL)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
@@ -172,9 +173,9 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test"))
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, STDALL)
reader, err := client.ContainerLogs(context.Background(), id, time.Time{}, container.STDALL)
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
@@ -185,10 +186,10 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy := new(mockedProxy)
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &container.Config{Tty: false}}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &docker.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
@@ -201,7 +202,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
_, err := client.FindContainer(context.Background(), "not_valid")
require.Error(t, err, "error should be thrown")
@@ -211,24 +212,24 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
proxy := new(mockedProxy)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &container.Config{Tty: false}}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &docker.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerRestart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
container, err := client.FindContainer(context.Background(), "abcdefghijkl")
c, err := client.FindContainer(context.Background(), "abcdefghijkl")
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, container.ID, "abcdefghijkl")
assert.Equal(t, c.ID, "abcdefghijkl")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
require.NoError(t, err, "error should not be thrown")
assert.Equal(t, err, nil)
}
@@ -239,18 +240,18 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
func Test_dockerClient_ContainerActions_error(t *testing.T) {
proxy := new(mockedProxy)
client := &httpClient{proxy, Host{ID: "localhost"}, system.Info{}}
client := &DockerClient{proxy, container.Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(types.ContainerJSON{}, errors.New("not found"))
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
container, err := client.FindContainer(context.Background(), "random-id")
c, err := client.FindContainer(context.Background(), "random-id")
require.Error(t, err, "error should be thrown")
actions := []string{"start", "stop", "restart"}
for _, action := range actions {
err := client.ContainerActions(context.Background(), ContainerAction(action), container.ID)
err := client.ContainerActions(context.Background(), container.ContainerAction(action), c.ID)
require.Error(t, err, "error should be thrown")
assert.Error(t, err, "error should have been returned")
}
+101
View File
@@ -0,0 +1,101 @@
package docker
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
"github.com/amir20/dozzle/internal/container"
)
var ErrBadHeader = errors.New("bad header")
type StdType int
const (
stdout StdType = iota
stderr
)
type LogReader struct {
reader *bufio.Reader
tty bool
pool *sync.Pool
}
func NewLogReader(r io.Reader, tty bool) *LogReader {
return &LogReader{
reader: bufio.NewReader(r),
tty: tty,
pool: &sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
},
}
}
func (d *LogReader) Read() (string, container.StdType, error) {
message, stdType, err := d.readEvent()
if err != nil {
return "", 0, err
}
var std container.StdType
switch stdType {
case stdout:
std = container.STDOUT
case stderr:
std = container.STDERR
}
return message, std, nil
}
func (d *LogReader) readEvent() (string, StdType, error) {
header := []byte{0, 0, 0, 0, 0, 0, 0, 0}
buffer := d.pool.Get().(*bytes.Buffer)
buffer.Reset()
defer d.pool.Put(buffer)
var streamType StdType = stdout
if d.tty {
message, err := d.reader.ReadString('\n')
if err != nil {
return message, streamType, err
}
return message, streamType, nil
} else {
n, err := io.ReadFull(d.reader, header)
if err != nil {
return "", streamType, err
}
if n != 8 {
message, _ := d.reader.ReadString('\n')
return message, streamType, ErrBadHeader
}
switch header[0] {
case 1:
streamType = stdout
case 2:
streamType = stderr
}
count := binary.BigEndian.Uint32(header[4:])
if count == 0 {
return "", streamType, nil
}
_, err = io.CopyN(buffer, d.reader, int64(count))
if err != nil {
return "", streamType, err
}
return buffer.String(), streamType, nil
}
}
+20 -19
View File
@@ -8,35 +8,36 @@ import (
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog/log"
)
type StatsCollector struct {
stream chan ContainerStat
subscribers *xsync.MapOf[context.Context, chan<- ContainerStat]
client Client
type DockerStatsCollector struct {
stream chan container.ContainerStat
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
client container.Client
cancelers *xsync.MapOf[string, context.CancelFunc]
stopper context.CancelFunc
timer *time.Timer
mu sync.Mutex
totalStarted atomic.Int32
filter ContainerFilter
labels container.ContainerLabels
}
var timeToStop = 6 * time.Hour
func NewStatsCollector(client Client, filter ContainerFilter) *StatsCollector {
return &StatsCollector{
stream: make(chan ContainerStat),
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerStat](),
func NewDockerStatsCollector(client container.Client, labels container.ContainerLabels) *DockerStatsCollector {
return &DockerStatsCollector{
stream: make(chan container.ContainerStat),
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
client: client,
cancelers: xsync.NewMapOf[string, context.CancelFunc](),
filter: filter,
labels: labels,
}
}
func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerStat) {
func (c *DockerStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
c.subscribers.Store(ctx, stats)
go func() {
<-ctx.Done()
@@ -44,7 +45,7 @@ func (c *StatsCollector) Subscribe(ctx context.Context, stats chan<- ContainerSt
}()
}
func (c *StatsCollector) forceStop() {
func (c *DockerStatsCollector) forceStop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.stopper != nil {
@@ -54,7 +55,7 @@ func (c *StatsCollector) forceStop() {
}
}
func (c *StatsCollector) Stop() {
func (c *DockerStatsCollector) Stop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.totalStarted.Add(-1) == 0 {
@@ -64,7 +65,7 @@ func (c *StatsCollector) Stop() {
}
}
func (c *StatsCollector) reset() {
func (c *DockerStatsCollector) reset() {
c.mu.Lock()
defer c.mu.Unlock()
if c.timer != nil {
@@ -73,7 +74,7 @@ func (c *StatsCollector) reset() {
c.timer = nil
}
func streamStats(parent context.Context, sc *StatsCollector, id string) {
func streamStats(parent context.Context, sc *DockerStatsCollector, id string) {
ctx, cancel := context.WithCancel(parent)
sc.cancelers.Store(id, cancel)
log.Debug().Str("container", id).Str("host", sc.client.Host().Name).Msg("starting to stream stats")
@@ -86,7 +87,7 @@ func streamStats(parent context.Context, sc *StatsCollector, id string) {
}
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
func (sc *StatsCollector) Start(parentCtx context.Context) bool {
func (sc *DockerStatsCollector) Start(parentCtx context.Context) bool {
sc.reset()
sc.totalStarted.Add(1)
@@ -100,7 +101,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
sc.mu.Unlock()
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
if containers, err := sc.client.ListContainers(timeoutCtx, sc.filter); err == nil {
if containers, err := sc.client.ListContainers(timeoutCtx, sc.labels); err == nil {
for _, c := range containers {
if c.State == "running" {
go streamStats(ctx, sc, c.ID)
@@ -111,7 +112,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
}
cancel()
events := make(chan ContainerEvent)
events := make(chan container.ContainerEvent)
go func() {
log.Debug().Str("host", sc.client.Host().Name).Msg("starting to listen to docker events")
@@ -142,7 +143,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
log.Info().Str("host", sc.client.Host().Name).Msg("stopped container stats collector")
return true
case stat := <-sc.stream:
sc.subscribers.Range(func(c context.Context, stats chan<- ContainerStat) bool {
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
+40 -9
View File
@@ -4,39 +4,70 @@ import (
"context"
"testing"
"github.com/amir20/dozzle/internal/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func startedCollector(ctx context.Context) *StatsCollector {
type mockedClient struct {
mock.Mock
container.Client
}
func (m *mockedClient) ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error) {
args := m.Called(ctx, filter)
return args.Get(0).([]container.Container), args.Error(1)
}
func (m *mockedClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
args := m.Called(ctx, id)
return args.Get(0).(container.Container), args.Error(1)
}
func (m *mockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
args := m.Called(ctx, events)
return args.Error(0)
}
func (m *mockedClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
args := m.Called(ctx, id, stats)
return args.Error(0)
}
func (m *mockedClient) Host() container.Host {
args := m.Called()
return args.Get(0).(container.Host)
}
func startedCollector(ctx context.Context) *DockerStatsCollector {
client := new(mockedClient)
client.On("ListContainers", mock.Anything, mock.Anything).Return([]Container{
client.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{
{
ID: "1234",
Name: "test",
State: "running",
},
}, nil)
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- container.ContainerEvent")).
Return(nil).
Run(func(args mock.Arguments) {
ctx := args.Get(0).(context.Context)
<-ctx.Done()
})
client.On("ContainerStats", mock.Anything, mock.Anything, mock.AnythingOfType("chan<- docker.ContainerStat")).
client.On("ContainerStats", mock.Anything, mock.Anything, mock.AnythingOfType("chan<- container.ContainerStat")).
Return(nil).
Run(func(args mock.Arguments) {
stats := args.Get(2).(chan<- ContainerStat)
stats <- ContainerStat{
stats := args.Get(2).(chan<- container.ContainerStat)
stats <- container.ContainerStat{
ID: "1234",
}
})
client.On("Host").Return(Host{
client.On("Host").Return(container.Host{
ID: "localhost",
})
collector := NewStatsCollector(client, ContainerFilter{})
stats := make(chan ContainerStat)
collector := NewDockerStatsCollector(client, container.ContainerLabels{})
stats := make(chan container.ContainerStat)
collector.Subscribe(ctx, stats)
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"crypto/tls"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/rs/zerolog/log"
)
@@ -14,7 +14,7 @@ func RPCRequest(ctx context.Context, addr string, certs tls.Certificate) error {
if err != nil {
log.Fatal().Err(err).Msg("Failed to create agent client")
}
containers, err := client.ListContainers(ctx, docker.ContainerFilter{})
containers, err := client.ListContainers(ctx, container.ContainerLabels{})
log.Trace().Int("containers", len(containers)).Msg("Healtcheck RPC request completed")
return err
}
+236
View File
@@ -0,0 +1,236 @@
package k8s
import (
"context"
"fmt"
"io"
"strings"
"time"
"os"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/rs/zerolog/log"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
type K8sClient struct {
Clientset *kubernetes.Clientset
namespace string
config *rest.Config
host container.Host
}
func NewK8sClient(namespace string) (*K8sClient, error) {
var config *rest.Config
var err error
// Check if we're running in cluster
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
config, err = rest.InClusterConfig()
if err != nil {
return nil, err
}
log.Info().Msg("Running in-cluster mode")
} else {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = os.Getenv("HOME") + "/.kube/config"
}
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
log.Info().Msgf("Running in local mode with kubeconfig: %s", kubeconfig)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
nodes, err := clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
if len(nodes.Items) == 0 {
return nil, fmt.Errorf("nodes not found")
}
node := nodes.Items[0]
return &K8sClient{
Clientset: clientset,
namespace: namespace,
config: config,
host: container.Host{
ID: node.Status.NodeInfo.MachineID,
Name: node.Name,
},
}, nil
}
func podToContainers(pod *corev1.Pod) []container.Container {
started := time.Time{}
if pod.Status.StartTime != nil {
started = pod.Status.StartTime.Time
}
var containers []container.Container
for _, c := range pod.Spec.Containers {
containers = append(containers, container.Container{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
Name: pod.Name + "/" + c.Name,
Image: c.Image,
Created: pod.CreationTimestamp.Time,
State: phaseToState(pod.Status.Phase),
StartedAt: started,
Command: strings.Join(c.Command, " "),
Host: pod.Spec.NodeName,
Tty: c.TTY,
Stats: utils.NewRingBuffer[container.ContainerStat](300),
FullyLoaded: true,
})
}
return containers
}
func (k *K8sClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
pods, err := k.Clientset.CoreV1().Pods(k.namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
var containers []container.Container
for _, pod := range pods.Items {
containers = append(containers, podToContainers(&pod)...)
}
return containers, nil
}
func phaseToState(phase corev1.PodPhase) string {
switch phase {
case corev1.PodPending:
return "created"
case corev1.PodRunning:
return "running"
case corev1.PodSucceeded:
return "exited"
case corev1.PodFailed:
return "exited"
case corev1.PodUnknown:
return "unknown"
default:
return "unknown"
}
}
func (k *K8sClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
log.Debug().Str("id", id).Msg("Finding container")
namespace, podName, containerName := parsePodContainerID(id)
pod, err := k.Clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if err != nil {
return container.Container{}, err
}
for _, c := range podToContainers(pod) {
if c.ID == id {
return c, nil
}
}
return container.Container{}, fmt.Errorf("container %s not found in pod %s", containerName, podName)
}
func (k *K8sClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
namespace, podName, containerName := parsePodContainerID(id)
opts := &corev1.PodLogOptions{
Container: containerName,
Follow: true,
Previous: false,
Timestamps: true,
SinceTime: &metav1.Time{Time: since},
}
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
}
func (k *K8sClient) ContainerLogsBetweenDates(ctx context.Context, id string, start time.Time, end time.Time, stdType container.StdType) (io.ReadCloser, error) {
namespace, podName, containerName := parsePodContainerID(id)
opts := &corev1.PodLogOptions{
Container: containerName,
Follow: false,
Timestamps: true,
SinceTime: &metav1.Time{Time: start},
}
return k.Clientset.CoreV1().Pods(namespace).GetLogs(podName, opts).Stream(ctx)
}
func (k *K8sClient) ContainerEvents(ctx context.Context, ch chan<- container.ContainerEvent) error {
watch, err := k.Clientset.CoreV1().Pods(k.namespace).Watch(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for event := range watch.ResultChan() {
log.Debug().Interface("event.type", event.Type).Msg("Received kubernetes event")
pod, ok := event.Object.(*corev1.Pod)
if !ok {
continue
}
name := ""
switch event.Type {
case "ADDED":
name = "create"
case "DELETED":
name = "destroy"
case "MODIFIED":
name = "update"
}
for _, c := range podToContainers(pod) {
ch <- container.ContainerEvent{
Name: name,
ActorID: c.ID,
Host: pod.Spec.NodeName,
Time: time.Now(),
Container: &c,
}
}
}
return nil
}
func (k *K8sClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
panic("not implemented")
}
func (k *K8sClient) Ping(ctx context.Context) error {
_, err := k.Clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{Limit: 1})
return err
}
func (k *K8sClient) Host() container.Host {
return k.host
}
func (k *K8sClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
// Implementation for container actions (start, stop, restart, etc.)
return nil
}
// Helper function to parse pod and container names from container ID
func parsePodContainerID(id string) (string, string, string) {
parts := strings.Split(id, ":")
return parts[0], parts[1], parts[2]
}
+27
View File
@@ -0,0 +1,27 @@
package k8s
import (
"bufio"
"io"
"github.com/amir20/dozzle/internal/container"
)
type LogReader struct {
reader *bufio.Reader
}
func NewLogReader(reader io.ReadCloser) *LogReader {
return &LogReader{
reader: bufio.NewReader(reader),
}
}
func (r *LogReader) Read() (string, container.StdType, error) {
line, err := r.reader.ReadString('\n')
if err != nil {
return "", 0, err
}
return line, container.STDOUT, nil
}
+124
View File
@@ -0,0 +1,124 @@
package k8s
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
)
var timeToStop = 2 * time.Hour
type K8sStatsCollector struct {
client *K8sClient
metrics *metricsclient.Clientset
subscribers *xsync.MapOf[context.Context, chan<- container.ContainerStat]
stopper context.CancelFunc
timer *time.Timer
mu sync.Mutex
totalStarted atomic.Int32
labels container.ContainerLabels
}
func NewK8sStatsCollector(client *K8sClient, labels container.ContainerLabels) (*K8sStatsCollector, error) {
metricsClient, err := metricsclient.NewForConfig(client.config)
if err != nil {
return nil, err
}
return &K8sStatsCollector{
subscribers: xsync.NewMapOf[context.Context, chan<- container.ContainerStat](),
client: client,
labels: labels,
metrics: metricsClient,
}, nil
}
func (c *K8sStatsCollector) Subscribe(ctx context.Context, stats chan<- container.ContainerStat) {
c.subscribers.Store(ctx, stats)
go func() {
<-ctx.Done()
c.subscribers.Delete(ctx)
}()
}
func (c *K8sStatsCollector) Stop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.totalStarted.Add(-1) == 0 {
c.timer = time.AfterFunc(timeToStop, func() {
c.forceStop()
})
}
}
func (c *K8sStatsCollector) forceStop() {
c.mu.Lock()
defer c.mu.Unlock()
if c.stopper != nil {
c.stopper()
c.stopper = nil
log.Debug().Msg("stopped container k8s stats collector")
}
}
func (c *K8sStatsCollector) reset() {
c.mu.Lock()
defer c.mu.Unlock()
if c.timer != nil {
c.timer.Stop()
}
c.timer = nil
}
// Start starts the stats collector and blocks until it's stopped. It returns true if the collector was stopped, false if it was already running
func (sc *K8sStatsCollector) Start(parentCtx context.Context) bool {
sc.reset()
sc.totalStarted.Add(1)
sc.mu.Lock()
if sc.stopper != nil {
sc.mu.Unlock()
return false
}
var ctx context.Context
ctx, sc.stopper = context.WithCancel(parentCtx)
sc.mu.Unlock()
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
metricList, err := sc.metrics.MetricsV1beta1().PodMetricses(sc.client.namespace).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
for _, pod := range metricList.Items {
for _, c := range pod.Containers {
stat := container.ContainerStat{
ID: pod.Namespace + ":" + pod.Name + ":" + c.Name,
CPUPercent: float64(c.Usage.Cpu().MilliValue()) / 1000 * 100,
MemoryUsage: c.Usage.Memory().AsApproximateFloat64(),
}
log.Trace().Interface("stat", stat).Msg("k8s stats")
sc.subscribers.Range(func(c context.Context, stats chan<- container.ContainerStat) bool {
select {
case stats <- stat:
case <-c.Done():
sc.subscribers.Delete(c)
}
return true
})
}
}
case <-ctx.Done():
return true
}
}
}
+3 -3
View File
@@ -2,12 +2,12 @@ package cli
import (
"github.com/amir20/dozzle/internal/analytics"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/types"
"github.com/rs/zerolog/log"
)
func StartEvent(args Args, mode string, client docker.Client, subCommand string) {
func StartEvent(args Args, mode string, client container.Client, subCommand string) {
if args.NoAnalytics {
return
}
@@ -29,7 +29,7 @@ func StartEvent(args Args, mode string, client docker.Client, subCommand string)
host := client.Host()
event.ServerID = host.ID
event.ServerVersion = host.DockerVersion
event.IsSwarmMode = client.SystemInfo().Swarm.NodeID != ""
event.IsSwarmMode = host.Swarm
} else {
event.ServerID = "n/a"
}
+2 -1
View File
@@ -27,8 +27,9 @@ type Args struct {
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"3s" help:"sets the timeout for docker client"`
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"10s" help:"sets the timeout for docker client"`
Timeout time.Duration `arg:"-"`
Namespace string `arg:"env:DOZZLE_NAMESPACE" default:"" help:"sets the namespace to use in k8s"`
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
+7 -4
View File
@@ -4,19 +4,21 @@ import (
"context"
"embed"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
container_support "github.com/amir20/dozzle/internal/support/container"
docker_support "github.com/amir20/dozzle/internal/support/docker"
"github.com/rs/zerolog/log"
)
func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *docker_support.MultiHostService) {
var clients []docker_support.ClientService
func CreateMultiHostService(embeddedCerts embed.FS, args Args) *docker_support.MultiHostService {
var clients []container_support.ClientService
if len(args.RemoteHost) > 0 {
log.Info().Msg(`Consider using Dozzle's remote agent to manage remote hosts. See https://dozzle.dev/guide/agent for more information`)
}
for _, remoteHost := range args.RemoteHost {
host, err := docker.ParseConnection(remoteHost)
host, err := container.ParseConnection(remoteHost)
if err != nil {
log.Fatal().Err(err).Interface("host", remoteHost).Msg("Could not parse remote host")
}
@@ -46,6 +48,7 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *
log.Debug().Msg("Adding local Docker Engine")
clients = append(clients, docker_support.NewDockerClientService(localClient, args.Filter))
}
go StartEvent(args, "server", localClient, "")
}
certs, err := ReadCertificates(embeddedCerts)
@@ -54,5 +57,5 @@ func CreateMultiHostService(embeddedCerts embed.FS, args Args) (docker.Client, *
}
clientManager := docker_support.NewRetriableClientManager(args.RemoteAgent, args.Timeout, certs, clients...)
return localClient, docker_support.NewMultiHostService(clientManager, args.Timeout)
return docker_support.NewMultiHostService(clientManager, args.Timeout)
}
@@ -1,4 +1,4 @@
package docker_support
package container_support
import (
"context"
@@ -7,13 +7,13 @@ import (
"time"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/rs/zerolog/log"
)
type agentService struct {
client *agent.Client
host docker.Host
host container.Host
}
func NewAgentService(client *agent.Client) ClientService {
@@ -22,28 +22,28 @@ func NewAgentService(client *agent.Client) ClientService {
}
}
func (a *agentService) FindContainer(ctx context.Context, id string, filter docker.ContainerFilter) (docker.Container, error) {
func (a *agentService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return a.client.FindContainer(ctx, id)
}
func (a *agentService) RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
func (a *agentService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return a.client.StreamRawBytes(ctx, container.ID, from, to, stdTypes)
}
func (a *agentService) LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
func (a *agentService) LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
return a.client.LogsBetweenDates(ctx, container.ID, from, to, stdTypes)
}
func (a *agentService) StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
func (a *agentService) StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
return a.client.StreamContainerLogs(ctx, container.ID, from, stdTypes, events)
}
func (a *agentService) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error) {
log.Debug().Interface("filter", filter).Msg("Listing containers from agent")
return a.client.ListContainers(ctx, filter)
func (a *agentService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
log.Debug().Interface("labels", labels).Msg("Listing containers from agent")
return a.client.ListContainers(ctx, labels)
}
func (a *agentService) Host(ctx context.Context) (docker.Host, error) {
func (a *agentService) Host(ctx context.Context) (container.Host, error) {
host, err := a.client.Host(ctx)
if err != nil {
host := a.host
@@ -55,18 +55,18 @@ func (a *agentService) Host(ctx context.Context) (docker.Host, error) {
return a.host, err
}
func (a *agentService) SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat) {
func (a *agentService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
go a.client.StreamStats(ctx, stats)
}
func (a *agentService) SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent) {
func (a *agentService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
go a.client.StreamEvents(ctx, events)
}
func (d *agentService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container) {
func (d *agentService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
go d.client.StreamNewContainers(ctx, containers)
}
func (a *agentService) ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error {
func (a *agentService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return a.client.ContainerAction(ctx, container.ID, action)
}
@@ -0,0 +1,28 @@
package container_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
)
type ContainerFilter = func(*container.Container) bool
type ClientService interface {
FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error)
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
Host(ctx context.Context) (container.Host, error)
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error)
// Subscriptions
SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat)
SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent)
SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container)
// Blocking streaming functions that should be used in a goroutine
StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error
}
@@ -0,0 +1,37 @@
package container_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
)
type ContainerService struct {
clientService ClientService
Container container.Container
}
func NewContainerService(clientService ClientService, container container.Container) *ContainerService {
return &ContainerService{
clientService: clientService,
Container: container,
}
}
func (c *ContainerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
}
func (c *ContainerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
}
func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
}
func (c *ContainerService) Action(ctx context.Context, action container.ContainerAction) error {
return c.clientService.ContainerAction(ctx, c.Container, action)
}
-99
View File
@@ -1,99 +0,0 @@
package docker_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/docker"
)
type ClientService interface {
FindContainer(ctx context.Context, id string, filter docker.ContainerFilter) (docker.Container, error)
ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error)
Host(ctx context.Context) (docker.Host, error)
ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error
LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error)
RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error)
// Subscriptions
SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat)
SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent)
SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container)
// Blocking streaming functions that should be used in a goroutine
StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error
}
type dockerClientService struct {
client docker.Client
store *docker.ContainerStore
}
func NewDockerClientService(client docker.Client, filter docker.ContainerFilter) ClientService {
return &dockerClientService{
client: client,
store: docker.NewContainerStore(context.Background(), client, filter),
}
}
func (d *dockerClientService) RawLogs(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
return d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
}
func (d *dockerClientService) LogsBetweenDates(ctx context.Context, container docker.Container, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
g := docker.NewEventGenerator(ctx, reader, container)
return g.Events, nil
}
func (d *dockerClientService) StreamLogs(ctx context.Context, container docker.Container, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
reader, err := d.client.ContainerLogs(ctx, container.ID, from, stdTypes)
if err != nil {
return err
}
g := docker.NewEventGenerator(ctx, reader, container)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (d *dockerClientService) FindContainer(ctx context.Context, id string, filter docker.ContainerFilter) (docker.Container, error) {
return d.store.FindContainer(id, filter)
}
func (d *dockerClientService) ContainerAction(ctx context.Context, container docker.Container, action docker.ContainerAction) error {
return d.client.ContainerActions(ctx, action, container.ID)
}
func (d *dockerClientService) ListContainers(ctx context.Context, filter docker.ContainerFilter) ([]docker.Container, error) {
return d.store.ListContainers(filter)
}
func (d *dockerClientService) Host(ctx context.Context) (docker.Host, error) {
return d.client.Host(), nil
}
func (d *dockerClientService) SubscribeStats(ctx context.Context, stats chan<- docker.ContainerStat) {
d.store.SubscribeStats(ctx, stats)
}
func (d *dockerClientService) SubscribeEvents(ctx context.Context, events chan<- docker.ContainerEvent) {
d.store.SubscribeEvents(ctx, events)
}
func (d *dockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container) {
d.store.SubscribeNewContainers(ctx, containers)
}
@@ -1,30 +0,0 @@
package docker_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/docker"
)
type containerService struct {
clientService ClientService
Container docker.Container
}
func (c *containerService) RawLogs(ctx context.Context, from time.Time, to time.Time, stdTypes docker.StdType) (io.ReadCloser, error) {
return c.clientService.RawLogs(ctx, c.Container, from, to, stdTypes)
}
func (c *containerService) LogsBetweenDates(ctx context.Context, from time.Time, to time.Time, stdTypes docker.StdType) (<-chan *docker.LogEvent, error) {
return c.clientService.LogsBetweenDates(ctx, c.Container, from, to, stdTypes)
}
func (c *containerService) StreamLogs(ctx context.Context, from time.Time, stdTypes docker.StdType, events chan<- *docker.LogEvent) error {
return c.clientService.StreamLogs(ctx, c.Container, from, stdTypes, events)
}
func (c *containerService) Action(ctx context.Context, action docker.ContainerAction) error {
return c.clientService.ContainerAction(ctx, c.Container, action)
}
+111
View File
@@ -0,0 +1,111 @@
package docker_support
import (
"context"
"io"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
"github.com/docker/docker/pkg/stdcopy"
"github.com/rs/zerolog/log"
)
type DockerClientService struct {
client container.Client
store *container.ContainerStore
}
func NewDockerClientService(client container.Client, labels container.ContainerLabels) *DockerClientService {
statsCollector := docker.NewDockerStatsCollector(client, labels)
return &DockerClientService{
client: client,
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
func (d *DockerClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
in, out := io.Pipe()
go func() {
if container.Tty {
if _, err := io.Copy(out, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
}
} else {
if _, err := stdcopy.StdCopy(out, out, reader); err != nil {
log.Error().Err(err).Msgf("error copying logs for container %s", container.ID)
}
}
out.Close()
}()
return in, nil
}
func (d *DockerClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
reader, err := d.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(ctx, dockerReader, c)
return g.Events, nil
}
func (d *DockerClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
reader, err := d.client.ContainerLogs(ctx, c.ID, from, stdTypes)
if err != nil {
return err
}
dockerReader := docker.NewLogReader(reader, c.Tty)
g := container.NewEventGenerator(ctx, dockerReader, c)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (d *DockerClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return d.store.FindContainer(id, labels)
}
func (d *DockerClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return d.client.ContainerActions(ctx, action, container.ID)
}
func (d *DockerClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
return d.store.ListContainers(labels)
}
func (d *DockerClientService) Host(ctx context.Context) (container.Host, error) {
return d.client.Host(), nil
}
func (d *DockerClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
d.store.SubscribeStats(ctx, stats)
}
func (d *DockerClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
d.store.SubscribeEvents(ctx, events)
}
func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
d.store.SubscribeNewContainers(ctx, containers)
}
+32 -36
View File
@@ -5,14 +5,13 @@ import (
"fmt"
"time"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/rs/zerolog/log"
)
type ContainerFilter = func(*docker.Container) bool
type HostUnavailableError struct {
Host docker.Host
Host container.Host
Err error
}
@@ -21,12 +20,12 @@ func (h *HostUnavailableError) Error() string {
}
type ClientManager interface {
Find(id string) (ClientService, bool)
List() []ClientService
RetryAndList() ([]ClientService, []error)
Subscribe(ctx context.Context, channel chan<- docker.Host)
Hosts(ctx context.Context) []docker.Host
LocalClients() []docker.Client
Find(id string) (container_support.ClientService, bool)
List() []container_support.ClientService
RetryAndList() ([]container_support.ClientService, []error)
Subscribe(ctx context.Context, channel chan<- container.Host)
Hosts(ctx context.Context) []container.Host
LocalClients() []container.Client
}
type MultiHostService struct {
@@ -43,25 +42,22 @@ func NewMultiHostService(manager ClientManager, timeout time.Duration) *MultiHos
return m
}
func (m *MultiHostService) FindContainer(host string, id string, filter docker.ContainerFilter) (*containerService, error) {
func (m *MultiHostService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
client, ok := m.manager.Find(host)
if !ok {
return nil, fmt.Errorf("host %s not found", host)
}
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
container, err := client.FindContainer(ctx, id, filter)
container, err := client.FindContainer(ctx, id, labels)
if err != nil {
return nil, err
}
return &containerService{
clientService: client,
Container: container,
}, nil
return container_support.NewContainerService(client, container), nil
}
func (m *MultiHostService) ListContainersForHost(host string, filter docker.ContainerFilter) ([]docker.Container, error) {
func (m *MultiHostService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
client, ok := m.manager.Find(host)
if !ok {
return nil, fmt.Errorf("host %s not found", host)
@@ -69,17 +65,17 @@ func (m *MultiHostService) ListContainersForHost(host string, filter docker.Cont
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
return client.ListContainers(ctx, filter)
return client.ListContainers(ctx, labels)
}
func (m *MultiHostService) ListAllContainers(filter docker.ContainerFilter) ([]docker.Container, []error) {
containers := make([]docker.Container, 0)
func (m *MultiHostService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
containers := make([]container.Container, 0)
clients, errors := m.manager.RetryAndList()
for _, client := range clients {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
list, err := client.ListContainers(ctx, filter)
list, err := client.ListContainers(ctx, labels)
if err != nil {
host, _ := client.Host(ctx)
log.Debug().Err(err).Str("host", host.Name).Msg("error listing containers")
@@ -94,9 +90,9 @@ func (m *MultiHostService) ListAllContainers(filter docker.ContainerFilter) ([]d
return containers, errors
}
func (m *MultiHostService) ListAllContainersFiltered(userFilter docker.ContainerFilter, filter ContainerFilter) ([]docker.Container, []error) {
containers, err := m.ListAllContainers(userFilter)
filtered := make([]docker.Container, 0, len(containers))
func (m *MultiHostService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
containers, err := m.ListAllContainers(userLabels)
filtered := make([]container.Container, 0, len(containers))
for _, container := range containers {
if filter(&container) {
filtered = append(filtered, container)
@@ -105,15 +101,15 @@ func (m *MultiHostService) ListAllContainersFiltered(userFilter docker.Container
return filtered, err
}
func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events chan<- docker.ContainerEvent, stats chan<- docker.ContainerStat) {
func (m *MultiHostService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) {
for _, client := range m.manager.List() {
client.SubscribeEvents(ctx, events)
client.SubscribeStats(ctx, stats)
}
}
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- docker.Container, filter ContainerFilter) {
newContainers := make(chan docker.Container)
func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
newContainers := make(chan container.Container)
for _, client := range m.manager.List() {
client.SubscribeContainersStarted(ctx, newContainers)
}
@@ -135,29 +131,29 @@ func (m *MultiHostService) SubscribeContainersStarted(ctx context.Context, conta
}()
}
func (m *MultiHostService) TotalClients() int {
return len(m.manager.List())
}
func (m *MultiHostService) Hosts() []docker.Host {
func (m *MultiHostService) Hosts() []container.Host {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
return m.manager.Hosts(ctx)
}
func (m *MultiHostService) LocalHost() (docker.Host, error) {
func (m *MultiHostService) LocalHost() (container.Host, error) {
for _, host := range m.Hosts() {
if host.Type == "local" {
return host, nil
}
}
return docker.Host{}, fmt.Errorf("local host not found")
return container.Host{}, fmt.Errorf("local host not found")
}
func (m *MultiHostService) SubscribeAvailableHosts(ctx context.Context, hosts chan<- docker.Host) {
func (m *MultiHostService) SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host) {
m.manager.Subscribe(ctx, hosts)
}
func (m *MultiHostService) LocalClients() []docker.Client {
func (m *MultiHostService) LocalClients() []container.Client {
return m.manager.LocalClients()
}
func (m *MultiHostService) TotalClients() int {
return len(m.manager.List())
}
@@ -8,7 +8,9 @@ import (
"time"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/samber/lo"
lop "github.com/samber/lo/parallel"
@@ -17,16 +19,16 @@ import (
)
type RetriableClientManager struct {
clients map[string]ClientService
clients map[string]container_support.ClientService
failedAgents []string
certs tls.Certificate
mu sync.RWMutex
subscribers *xsync.MapOf[context.Context, chan<- docker.Host]
subscribers *xsync.MapOf[context.Context, chan<- container.Host]
timeout time.Duration
}
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...ClientService) *RetriableClientManager {
clientMap := make(map[string]ClientService)
func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls.Certificate, clients ...container_support.ClientService) *RetriableClientManager {
clientMap := make(map[string]container_support.ClientService)
for _, client := range clients {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@@ -64,7 +66,7 @@ func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls
if _, ok := clientMap[host.ID]; ok {
log.Warn().Str("host", host.Name).Str("id", host.ID).Msg("duplicate host with same ID found")
} else {
clientMap[host.ID] = NewAgentService(agent)
clientMap[host.ID] = container_support.NewAgentService(agent)
}
}
@@ -72,12 +74,12 @@ func NewRetriableClientManager(agents []string, timeout time.Duration, certs tls
clients: clientMap,
failedAgents: failed,
certs: certs,
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
subscribers: xsync.NewMapOf[context.Context, chan<- container.Host](),
timeout: timeout,
}
}
func (m *RetriableClientManager) Subscribe(ctx context.Context, channel chan<- docker.Host) {
func (m *RetriableClientManager) Subscribe(ctx context.Context, channel chan<- container.Host) {
m.subscribers.Store(ctx, channel)
go func() {
@@ -86,7 +88,7 @@ func (m *RetriableClientManager) Subscribe(ctx context.Context, channel chan<- d
}()
}
func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
func (m *RetriableClientManager) RetryAndList() ([]container_support.ClientService, []error) {
m.mu.Lock()
errors := make([]error, 0)
if len(m.failedAgents) > 0 {
@@ -110,12 +112,12 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
m.clients[host.ID] = NewAgentService(agent)
m.subscribers.Range(func(ctx context.Context, channel chan<- docker.Host) bool {
m.clients[host.ID] = container_support.NewAgentService(agent)
m.subscribers.Range(func(ctx context.Context, channel chan<- container.Host) bool {
host.Available = true
// We don't want to block the subscribers in event.go
go func(host docker.Host) {
go func(host container.Host) {
select {
case channel <- host:
case <-ctx.Done():
@@ -133,14 +135,14 @@ func (m *RetriableClientManager) RetryAndList() ([]ClientService, []error) {
return m.List(), errors
}
func (m *RetriableClientManager) List() []ClientService {
func (m *RetriableClientManager) List() []container_support.ClientService {
m.mu.RLock()
defer m.mu.RUnlock()
return lo.Values(m.clients)
}
func (m *RetriableClientManager) Find(id string) (ClientService, bool) {
func (m *RetriableClientManager) Find(id string) (container_support.ClientService, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -152,10 +154,10 @@ func (m *RetriableClientManager) String() string {
return fmt.Sprintf("RetriableClientManager{clients: %d, failedAgents: %d}", len(m.clients), len(m.failedAgents))
}
func (m *RetriableClientManager) Hosts(ctx context.Context) []docker.Host {
func (m *RetriableClientManager) Hosts(ctx context.Context) []container.Host {
clients := m.List()
hosts := lop.Map(clients, func(client ClientService, _ int) docker.Host {
hosts := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("host", host.Name).Msg("error fetching host info for client")
@@ -168,7 +170,7 @@ func (m *RetriableClientManager) Hosts(ctx context.Context) []docker.Host {
})
for _, endpoint := range m.failedAgents {
hosts = append(hosts, docker.Host{
hosts = append(hosts, container.Host{
ID: endpoint,
Name: endpoint,
Endpoint: endpoint,
@@ -180,13 +182,13 @@ func (m *RetriableClientManager) Hosts(ctx context.Context) []docker.Host {
return hosts
}
func (m *RetriableClientManager) LocalClients() []docker.Client {
func (m *RetriableClientManager) LocalClients() []container.Client {
services := m.List()
clients := make([]docker.Client, 0)
clients := make([]container.Client, 0)
for _, service := range services {
if clientService, ok := service.(*dockerClientService); ok {
if clientService, ok := service.(*DockerClientService); ok {
clients = append(clients, clientService.client)
}
}
+23 -20
View File
@@ -10,7 +10,10 @@ import (
"time"
"github.com/amir20/dozzle/internal/agent"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/puzpuzpuz/xsync/v3"
"github.com/samber/lo"
lop "github.com/samber/lo/parallel"
@@ -19,11 +22,11 @@ import (
)
type SwarmClientManager struct {
clients map[string]ClientService
clients map[string]container_support.ClientService
certs tls.Certificate
mu sync.RWMutex
subscribers *xsync.MapOf[context.Context, chan<- docker.Host]
localClient docker.Client
subscribers *xsync.MapOf[context.Context, chan<- container.Host]
localClient container.Client
localIPs []string
name string
timeout time.Duration
@@ -47,9 +50,9 @@ func localIPs() []string {
return ips
}
func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, filter docker.ContainerFilter) *SwarmClientManager {
clientMap := make(map[string]ClientService)
localService := NewDockerClientService(localClient, filter)
func NewSwarmClientManager(localClient *docker.DockerClient, certs tls.Certificate, timeout time.Duration, agentManager *RetriableClientManager, labels container.ContainerLabels) *SwarmClientManager {
clientMap := make(map[string]container_support.ClientService)
localService := NewDockerClientService(localClient, labels)
clientMap[localClient.Host().ID] = localService
id, ok := os.LookupEnv("HOSTNAME")
@@ -59,12 +62,12 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, tim
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
container, err := localClient.FindContainer(ctx, id)
c, err := localClient.FindContainer(ctx, id)
if err != nil {
log.Fatal().Err(err).Msg("error finding own container when looking for swarm service name")
}
serviceName := container.Labels["com.docker.swarm.service.name"]
serviceName := c.Labels["com.docker.swarm.service.name"]
log.Debug().Str("service", serviceName).Msg("found swarm service name")
@@ -72,7 +75,7 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, tim
localClient: localClient,
clients: clientMap,
certs: certs,
subscribers: xsync.NewMapOf[context.Context, chan<- docker.Host](),
subscribers: xsync.NewMapOf[context.Context, chan<- container.Host](),
localIPs: localIPs(),
name: serviceName,
timeout: timeout,
@@ -80,7 +83,7 @@ func NewSwarmClientManager(localClient docker.Client, certs tls.Certificate, tim
}
}
func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- docker.Host) {
func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- container.Host) {
m.subscribers.Store(ctx, channel)
m.agentManager.Subscribe(ctx, channel)
@@ -90,7 +93,7 @@ func (m *SwarmClientManager) Subscribe(ctx context.Context, channel chan<- docke
}()
}
func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
func (m *SwarmClientManager) RetryAndList() ([]container_support.ClientService, []error) {
m.mu.Lock()
ips, err := net.LookupIP(fmt.Sprintf("tasks.%s", m.name))
@@ -104,7 +107,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
}
clients := lo.Values(m.clients)
endpoints := lo.KeyBy(clients, func(client ClientService) string {
endpoints := lo.KeyBy(clients, func(client container_support.ClientService) string {
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
host, _ := client.Host(ctx)
@@ -155,11 +158,11 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
continue
}
client := NewAgentService(agent)
client := container_support.NewAgentService(agent)
m.clients[host.ID] = client
log.Info().Stringer("ip", ip).Str("id", host.ID).Str("name", host.Name).Msg("added new swarm agent")
m.subscribers.Range(func(ctx context.Context, channel chan<- docker.Host) bool {
m.subscribers.Range(func(ctx context.Context, channel chan<- container.Host) bool {
host.Available = true
host.Type = "swarm"
@@ -182,7 +185,7 @@ func (m *SwarmClientManager) RetryAndList() ([]ClientService, []error) {
return m.List(), errors
}
func (m *SwarmClientManager) List() []ClientService {
func (m *SwarmClientManager) List() []container_support.ClientService {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -192,7 +195,7 @@ func (m *SwarmClientManager) List() []ClientService {
return append(agents, clients...)
}
func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
func (m *SwarmClientManager) Find(id string) (container_support.ClientService, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -205,12 +208,12 @@ func (m *SwarmClientManager) Find(id string) (ClientService, bool) {
return client, ok
}
func (m *SwarmClientManager) Hosts(ctx context.Context) []docker.Host {
func (m *SwarmClientManager) Hosts(ctx context.Context) []container.Host {
m.mu.RLock()
clients := lo.Values(m.clients)
m.mu.RUnlock()
swarmNodes := lop.Map(clients, func(client ClientService, _ int) docker.Host {
swarmNodes := lop.Map(clients, func(client container_support.ClientService, _ int) container.Host {
host, err := client.Host(ctx)
if err != nil {
log.Warn().Err(err).Str("id", host.ID).Msg("error getting host from client")
@@ -230,6 +233,6 @@ func (m *SwarmClientManager) String() string {
return fmt.Sprintf("SwarmClientManager{clients: %d}", len(m.clients))
}
func (m *SwarmClientManager) LocalClients() []docker.Client {
return []docker.Client{m.localClient}
func (m *SwarmClientManager) LocalClients() []container.Client {
return []container.Client{m.localClient}
}
+133
View File
@@ -0,0 +1,133 @@
package k8s_support
import (
"context"
"fmt"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/k8s"
container_support "github.com/amir20/dozzle/internal/support/container"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type K8sClusterService struct {
client *K8sClientService
timeout time.Duration
hosts []container.Host
}
func NewK8sClusterService(client *k8s.K8sClient, timeout time.Duration) (*K8sClusterService, error) {
hosts := make([]container.Host, 0)
nodes, err := client.Clientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, err
}
if len(nodes.Items) == 0 {
return nil, fmt.Errorf("nodes not found")
}
for _, node := range nodes.Items {
hosts = append(hosts, container.Host{
ID: node.Name,
Name: node.Name,
MemTotal: node.Status.Capacity.Memory().Value(),
NCPU: int(node.Status.Capacity.Cpu().Value()),
DockerVersion: node.Status.NodeInfo.ContainerRuntimeVersion,
Type: "k8s",
Available: true,
})
}
return &K8sClusterService{
client: NewK8sClientService(client, container.ContainerLabels{}),
timeout: timeout,
hosts: hosts,
}, nil
}
func (m *K8sClusterService) FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error) {
container, err := m.client.FindContainer(context.Background(), id, labels)
if err != nil {
return nil, err
}
return container_support.NewContainerService(m.client, container), nil
}
func (m *K8sClusterService) ListContainersForHost(host string, labels container.ContainerLabels) ([]container.Container, error) {
containers, err := m.client.ListContainers(context.Background(), labels)
if err != nil {
return nil, err
}
filteredContainers := make([]container.Container, 0)
for _, container := range containers {
if container.Host == host {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers, nil
}
func (m *K8sClusterService) ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error) {
containers, err := m.client.ListContainers(context.Background(), labels)
if err != nil {
return nil, []error{err}
}
return containers, nil
}
func (m *K8sClusterService) ListAllContainersFiltered(userLabels container.ContainerLabels, filter container_support.ContainerFilter) ([]container.Container, []error) {
containers, err := m.ListAllContainers(userLabels)
filtered := make([]container.Container, 0, len(containers))
for _, container := range containers {
if filter(&container) {
filtered = append(filtered, container)
}
}
return filtered, err
}
func (m *K8sClusterService) SubscribeEventsAndStats(ctx context.Context, events chan<- container.ContainerEvent, stats chan<- container.ContainerStat) {
m.client.SubscribeEvents(ctx, events)
m.client.SubscribeStats(ctx, stats)
}
func (m *K8sClusterService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container, filter container_support.ContainerFilter) {
newContainers := make(chan container.Container)
m.client.SubscribeContainersStarted(ctx, newContainers)
go func() {
<-ctx.Done()
close(newContainers)
}()
go func() {
for container := range newContainers {
if filter(&container) {
select {
case containers <- container:
case <-ctx.Done():
return
}
}
}
}()
}
func (m *K8sClusterService) Hosts() []container.Host {
return m.hosts
}
func (m *K8sClusterService) LocalHost() (container.Host, error) {
return m.client.client.Host(), nil
}
func (m *K8sClusterService) SubscribeAvailableHosts(ctx context.Context, hosts chan<- container.Host) {
}
func (m *K8sClusterService) LocalClients() []container.Client {
return []container.Client{m.client.client}
}
+92
View File
@@ -0,0 +1,92 @@
package k8s_support
import (
"context"
"io"
"github.com/rs/zerolog/log"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/k8s"
)
type K8sClientService struct {
client *k8s.K8sClient
store *container.ContainerStore
}
func NewK8sClientService(client *k8s.K8sClient, labels container.ContainerLabels) *K8sClientService {
statsCollector, err := k8s.NewK8sStatsCollector(client, labels)
if err != nil {
log.Fatal().Err(err).Msg("Could not create k8s stats collector")
}
return &K8sClientService{
client: client,
store: container.NewContainerStore(context.Background(), client, statsCollector, labels),
}
}
func (k *K8sClientService) FindContainer(ctx context.Context, id string, labels container.ContainerLabels) (container.Container, error) {
return k.store.FindContainer(id, labels)
}
func (k *K8sClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
return k.store.ListContainers(labels)
}
func (k *K8sClientService) Host(ctx context.Context) (container.Host, error) {
return k.client.Host(), nil
}
func (k *K8sClientService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error {
return k.client.ContainerActions(ctx, action, container.ID)
}
func (k *K8sClientService) LogsBetweenDates(ctx context.Context, c container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) {
reader, err := k.client.ContainerLogsBetweenDates(ctx, c.ID, from, to, stdTypes)
if err != nil {
return nil, err
}
k8sReader := k8s.NewLogReader(reader)
g := container.NewEventGenerator(ctx, k8sReader, c)
return g.Events, nil
}
func (k *K8sClientService) RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) {
return k.client.ContainerLogsBetweenDates(ctx, container.ID, from, to, stdTypes)
}
func (k *K8sClientService) StreamLogs(ctx context.Context, c container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error {
reader, err := k.client.ContainerLogs(ctx, c.ID, from, stdTypes)
if err != nil {
return err
}
k8sReader := k8s.NewLogReader(reader)
g := container.NewEventGenerator(ctx, k8sReader, c)
for event := range g.Events {
events <- event
}
select {
case e := <-g.Errors:
return e
default:
return nil
}
}
func (k *K8sClientService) SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) {
k.store.SubscribeStats(ctx, stats)
}
func (k *K8sClientService) SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) {
k.store.SubscribeEvents(ctx, events)
}
func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) {
k.store.SubscribeNewContainers(ctx, containers)
}
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
)
@@ -26,7 +26,7 @@ func ParseRegex(search string) (*regexp.Regexp, error) {
return re, nil
}
func Search(re *regexp.Regexp, logEvent *docker.LogEvent) bool {
func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
switch value := logEvent.Message.(type) {
case string:
if re.MatchString(value) {
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/container"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
@@ -15,22 +15,22 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
id := chi.URLParam(r, "id")
usersFilter := h.config.Filter
userLabels := h.config.Labels
if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context())
if user.ContainerFilter.Exists() {
usersFilter = user.ContainerFilter
if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels
}
}
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)
if err != nil {
log.Error().Err(err).Msg("error while trying to find container")
http.Error(w, err.Error(), http.StatusNotFound)
return
}
parsedAction, err := docker.ParseContainerAction(action)
parsedAction, err := container.ParseContainerAction(action)
if err != nil {
log.Error().Err(err).Msg("error while trying to parse action")
http.Error(w, err.Error(), http.StatusBadRequest)

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