From d24e8fe7f310b423ef67f9627717b88a21483ce3 Mon Sep 17 00:00:00 2001 From: Craig Furman Date: Wed, 31 Jul 2024 18:26:56 +0100 Subject: [PATCH] feat(appliance): backport all recent appliance changes (#64182) Draft in case plan in https://linear.app/sourcegraph/issue/REL-309/release-process-for-appliance not agreed. Please see that first. Generated by: ``` git log --format=%H d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance internal/appliance docker-images/appliance-frontend | tac | xargs git cherry-pick ``` d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c being the commit we branched off main from to create the 5.5.x branch (https://buildkite.com/sourcegraph/sourcegraph/builds/281882). Commits (generated by `git log --format='- https://github.com/sourcegraph/sourcegraph/commit/%H' d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance internal/appliance docker-images/appliance-frontend | tac`): - https://github.com/sourcegraph/sourcegraph/commit/a20b0650b453d11d7dc428b9f919b79465db088c - https://github.com/sourcegraph/sourcegraph/commit/b71c986c77829cfe3bdc6f2d31e8590fdb969b17 - https://github.com/sourcegraph/sourcegraph/commit/91864283bc0811928188f415e6cda7d3fe940e34 - https://github.com/sourcegraph/sourcegraph/commit/c88b57020fa49fe996e37ca9409a541d48d7a63e - https://github.com/sourcegraph/sourcegraph/commit/0491839942c3087687d0f48ade1c544f1fbcb881 - https://github.com/sourcegraph/sourcegraph/commit/619fc5707415bd50631153bcb1b01c670d44fd82 - https://github.com/sourcegraph/sourcegraph/commit/e81c39a834810a60c8827456963511213eb5f602 - https://github.com/sourcegraph/sourcegraph/commit/a61f353e0e93f848fa476ffda6d46f71060d465d - https://github.com/sourcegraph/sourcegraph/commit/0abef7b43d5dadac92a3ff318f3caf6d7b572f6a - https://github.com/sourcegraph/sourcegraph/commit/0e391a964ae8d21904bd875862759db521feb5ba - https://github.com/sourcegraph/sourcegraph/commit/daae9adfb642f2c73e7b4b2dcb7acca618ae86d3 - https://github.com/sourcegraph/sourcegraph/commit/6e31f0f4cc89ad7d3892e67bad55ab9a0b9658a7 - https://github.com/sourcegraph/sourcegraph/commit/49a600220d5eb2875ab54c87329d0a90f15c38d6 - https://github.com/sourcegraph/sourcegraph/commit/37cf4a7b7e9dfb4f09635d2e04ce30daf5d8b7a0 - https://github.com/sourcegraph/sourcegraph/commit/29fc613c376fe38055fa907c5bbd293ea6e4408f - https://github.com/sourcegraph/sourcegraph/commit/255e6387cc7854c8aed6dfa1445fcade380ec690 - https://github.com/sourcegraph/sourcegraph/commit/49b32fcf3a1ec8362e3be286de182aba76f93917 - https://github.com/sourcegraph/sourcegraph/commit/9f4c160f91838ed3167a38ba62b7ca90ad90a771 - https://github.com/sourcegraph/sourcegraph/commit/3814fd7390455354be1129cdb1b240c201ef5964 - https://github.com/sourcegraph/sourcegraph/commit/c68e92bc28b3ce0878aebe8e10471cca89010b77 - https://github.com/sourcegraph/sourcegraph/commit/7e82c27ab5de1c76f1e877ec9dee665800807cbb - https://github.com/sourcegraph/sourcegraph/commit/98c6b9703f889bb8b706855204276d3528b03356 - https://github.com/sourcegraph/sourcegraph/commit/a01ebad8417d4c14b969f61a3a6ad8e836728047 - https://github.com/sourcegraph/sourcegraph/commit/8c2d8da234cc46b529b8df92a38cdf7f7d83a7a7 - https://github.com/sourcegraph/sourcegraph/commit/ebec72d7ed7046406d22d2bf8276612945db358d - https://github.com/sourcegraph/sourcegraph/commit/d945f192852a7dc3cd2482602fe37f614f82f8a1 - https://github.com/sourcegraph/sourcegraph/commit/84e28998e9d3976b79d858439953d101ca44ab7e ## Test plan Tests pass. ## Changelog - Backport all recent appliance changes. The appliance is still pre-release. --------- Co-authored-by: Jacob Pleiness Co-authored-by: Anish Lakhwara Co-authored-by: Warren Gifford Co-authored-by: Nelson Araujo --- client/web/dev/utils/create-js-context.ts | 1 + .../site-admin/SiteAdminSidebar.story.tsx | 4 + client/web/src/integration/jscontext.ts | 1 + client/web/src/jscontext.ts | 5 + client/web/src/routes.tsx | 1 + client/web/src/site-admin/SiteAdminArea.tsx | 4 + .../web/src/site-admin/SiteAdminSidebar.tsx | 1 + client/web/src/site-admin/sidebaritems.ts | 6 + cmd/appliance/shared/BUILD.bazel | 3 + cmd/appliance/shared/config.go | 20 +- cmd/appliance/shared/shared.go | 55 +- .../internal/app/jscontext/jscontext.go | 2 + deps.bzl | 7 - dev/BUILD.bazel | 1 + docker-images/appliance-frontend/BUILD.bazel | 71 ++ .../appliance-frontend/image_test.yaml | 14 + docker-images/appliance-frontend/init.sh | 18 + docker-images/appliance-frontend/nginx.conf | 16 + go.mod | 5 +- go.sum | 2 - internal/appliance/BUILD.bazel | 25 +- internal/appliance/appliance.go | 168 ++-- internal/appliance/auth.go | 68 +- internal/appliance/auth_test.go | 174 ++-- internal/appliance/config/BUILD.bazel | 2 + internal/appliance/config/annotations.go | 36 +- internal/appliance/config/config.go | 22 +- internal/appliance/config/embed.go | 3 + internal/appliance/config/otel/agent.yaml | 43 + internal/appliance/config/spec.go | 37 +- internal/appliance/embed.go | 15 - internal/appliance/errors.go | 56 +- .../frontend/maintenance/BUILD.bazel | 42 +- .../appliance/frontend/maintenance/README.md | 14 + .../frontend/maintenance/image_test.yaml | 12 + .../maintenance/maintenance.conf.template | 49 + .../frontend/maintenance/src/Frame.tsx | 16 +- .../frontend/maintenance/src/Home.tsx | 2 + .../frontend/maintenance/src/Install.tsx | 240 +++-- .../frontend/maintenance/src/Login.tsx | 2 +- .../frontend/maintenance/src/Maintenance.tsx | 21 +- .../maintenance/src/OperatorDebugBar.tsx | 112 --- .../maintenance/src/OperatorStatus.tsx | 8 +- .../frontend/maintenance/src/Progress.tsx | 10 +- .../frontend/maintenance/src/Theme.tsx | 4 +- .../frontend/maintenance/src/WaitForAdmin.tsx | 35 +- .../frontend/maintenance/src/debugBar.ts | 35 - .../maintenance/src/reportWebVitals.ts | 6 +- .../frontend/maintenance/src/state.ts | 31 + internal/appliance/healthchecker/BUILD.bazel | 44 + .../appliance/healthchecker/health_checker.go | 95 ++ .../healthchecker/health_checker_test.go | 164 ++++ internal/appliance/healthchecker/probe.go | 38 + internal/appliance/html.go | 187 ---- internal/appliance/json.go | 213 +++++ internal/appliance/json_test.go | 230 +++++ internal/appliance/k8senvtest/BUILD.bazel | 7 +- internal/appliance/k8senvtest/README.md | 17 + internal/appliance/k8senvtest/envtest.go | 8 + internal/appliance/k8senvtest/namespaces.go | 34 + internal/appliance/maintenance/README.md | 88 -- .../maintenance/backend/api/BUILD.bazel | 5 +- .../maintenance/backend/api/install.go | 12 +- .../maintenance/backend/api/stage.go | 40 +- .../appliance/maintenance/backend/api/task.go | 28 +- .../maintenance/backend/cmd/BUILD.bazel | 15 - .../appliance/maintenance/backend/cmd/main.go | 13 - .../maintenance/backend/operator/BUILD.bazel | 11 - .../maintenance/backend/operator/manage.go | 58 -- .../maintenance/backend/operator/task.go | 13 - internal/appliance/maintenance/demo.html | 151 --- .../appliance/maintenance/helm/.helmignore | 23 - .../appliance/maintenance/helm/Chart.yaml | 8 - .../maintenance/helm/templates/NOTES.txt | 51 - .../helm/templates/api-deploy.yaml | 31 - .../helm/templates/api-secret.yaml | 10 - .../helm/templates/api-service.yaml | 14 - .../maintenance/helm/templates/namespace.yaml | 4 - .../maintenance/helm/templates/ui-deploy.yaml | 26 - .../helm/templates/ui-service.yaml | 14 - .../appliance/maintenance/helm/values.yaml | 9 - internal/appliance/reconciler/BUILD.bazel | 11 + internal/appliance/reconciler/frontend.go | 210 ++++- .../appliance/reconciler/frontend_test.go | 338 +++++++ internal/appliance/reconciler/gitserver.go | 19 +- internal/appliance/reconciler/helpers_test.go | 27 +- .../appliance/reconciler/indexed_search.go | 2 +- internal/appliance/reconciler/kubernetes.go | 57 +- internal/appliance/reconciler/otel_agent.go | 102 ++ .../appliance/reconciler/otel_agent_test.go | 14 + .../reconciler/precise_code_intel.go | 4 +- internal/appliance/reconciler/reconcile.go | 19 +- internal/appliance/reconciler/repo_updater.go | 19 +- internal/appliance/reconciler/searcher.go | 19 +- .../appliance/reconciler/secret_management.go | 122 +++ .../reconciler/standard_config_test.go | 43 + internal/appliance/reconciler/symbols.go | 19 +- internal/appliance/reconciler/syntect.go | 4 +- .../golden-fixtures/blobstore/default.yaml | 5 +- .../golden-fixtures/cadvisor/default.yaml | 5 +- .../golden-fixtures/codeinsights/default.yaml | 5 +- .../golden-fixtures/codeintel/default.yaml | 5 +- .../frontend/adopt-service.yaml | 586 ++++++++++++ ...er-create-codeinsights-db-auth-secret.yaml | 567 +++++++++++ ...after-create-codeintel-db-auth-secret.yaml | 567 +++++++++++ .../after-create-pgsql-auth-secret.yaml | 567 +++++++++++ .../after-create-redis-cache-secret.yaml | 563 +++++++++++ .../after-create-redis-store-secret.yaml | 563 +++++++++++ .../golden-fixtures/frontend/default.yaml | 12 +- .../frontend/with-blobstore.yaml | 12 +- .../with-ingress-optional-fields.yaml | 15 +- .../frontend/with-ingress.yaml | 13 +- .../frontend/with-overrides.yaml | 12 +- .../golden-fixtures/gitserver/default.yaml | 6 +- .../golden-fixtures/grafana/default.yaml | 5 +- .../grafana/with-existing-configmap.yaml | 5 +- .../grafana/with-replicas.yaml | 5 +- .../indexed-search/default.yaml | 5 +- .../indexed-search/with-replicas.yaml | 5 +- .../golden-fixtures/jaeger/default.yaml | 5 +- .../golden-fixtures/jaeger/with-replicas.yaml | 5 +- .../golden-fixtures/otel-agent/default.yaml | 284 ++++++ .../golden-fixtures/pgsql/default.yaml | 5 +- .../precise-code-intel/default.yaml | 5 +- .../precise-code-intel/with-blobstore.yaml | 5 +- .../precise-code-intel/with-num-workers.yaml | 5 +- .../precise-code-intel/with-replicas.yaml | 5 +- .../golden-fixtures/prometheus/default.yaml | 5 +- .../prometheus/privileged.yaml | 5 +- .../prometheus/subsequent-disable.yaml | 5 +- .../prometheus/with-existing-configmap.yaml | 5 +- .../golden-fixtures/redis/default.yaml | 5 +- .../golden-fixtures/repo-updater/default.yaml | 6 +- .../golden-fixtures/searcher/default.yaml | 6 +- .../searcher/with-replicas.yaml | 6 +- .../searcher/with-storage.yaml | 6 +- .../blobstore-subsequent-disable.yaml | 5 +- .../blobstore-with-named-storage-class.yaml | 5 +- ...frontend-with-no-cpu-memory-resources.yaml | 12 +- .../precise-code-intel-with-env-vars.yaml | 5 +- .../redis-with-multiple-custom-images.yaml | 5 +- .../standard/redis-with-storage.yaml | 5 +- .../repo-updater-with-no-resources.yaml | 6 +- ...repo-updater-with-pod-template-config.yaml | 6 +- .../standard/repo-updater-with-resources.yaml | 6 +- .../repo-updater-with-sa-annotations.yaml | 6 +- .../standard/symbols-with-custom-image.yaml | 6 +- .../golden-fixtures/symbols/default.yaml | 6 +- .../golden-fixtures/symbols/with-storage.yaml | 6 +- .../golden-fixtures/syntect/default.yaml | 5 +- .../syntect/with-replicas.yaml | 5 +- .../golden-fixtures/worker/default.yaml | 6 +- .../worker/with-blobstore.yaml | 6 +- .../golden-fixtures/worker/with-replicas.yaml | 6 +- .../testdata/sg/blobstore/default.yaml | 5 +- .../testdata/sg/cadvisor/default.yaml | 5 +- .../testdata/sg/codeinsights/default.yaml | 5 +- .../testdata/sg/codeintel/default.yaml | 5 +- .../testdata/sg/frontend/default.yaml | 5 +- .../testdata/sg/frontend/with-blobstore.yaml | 5 +- .../with-ingress-optional-fields.yaml | 5 +- .../testdata/sg/frontend/with-ingress.yaml | 5 +- .../testdata/sg/frontend/with-overrides.yaml | 5 +- .../testdata/sg/gitserver/default.yaml | 5 +- .../testdata/sg/grafana/default.yaml | 5 +- .../sg/grafana/with-existing-configmap.yaml | 5 +- .../testdata/sg/grafana/with-replicas.yaml | 5 +- .../testdata/sg/indexed-search/default.yaml | 5 +- .../sg/indexed-search/with-replicas.yaml | 5 +- .../testdata/sg/jaeger/default.yaml | 5 +- .../testdata/sg/jaeger/with-replicas.yaml | 5 +- .../testdata/sg/otel-agent/default.yaml | 64 ++ .../reconciler/testdata/sg/pgsql/default.yaml | 5 +- .../sg/precise-code-intel/default.yaml | 5 +- .../sg/precise-code-intel/with-blobstore.yaml | 5 +- .../precise-code-intel/with-num-workers.yaml | 5 +- .../sg/precise-code-intel/with-replicas.yaml | 5 +- .../testdata/sg/prometheus/default.yaml | 5 +- .../testdata/sg/prometheus/privileged.yaml | 5 +- .../prometheus/with-existing-configmap.yaml | 5 +- .../reconciler/testdata/sg/redis/default.yaml | 5 +- .../testdata/sg/repo-updater/default.yaml | 5 +- .../testdata/sg/searcher/default.yaml | 5 +- .../testdata/sg/searcher/with-replicas.yaml | 5 +- .../testdata/sg/searcher/with-storage.yaml | 5 +- .../blobstore-with-named-storage-class.yaml | 5 +- .../sg/standard/everything-disabled.yaml | 5 +- ...frontend-with-no-cpu-memory-resources.yaml | 5 +- .../precise-code-intel-with-env-vars.yaml | 5 +- .../redis-with-multiple-custom-images.yaml | 5 +- .../sg/standard/redis-with-storage.yaml | 5 +- .../repo-updater-with-no-resources.yaml | 5 +- ...repo-updater-with-pod-template-config.yaml | 5 +- .../standard/repo-updater-with-resources.yaml | 5 +- .../repo-updater-with-sa-annotations.yaml | 5 +- .../standard/symbols-with-custom-image.yaml | 5 +- .../testdata/sg/symbols/default.yaml | 5 +- .../testdata/sg/symbols/with-storage.yaml | 5 +- .../testdata/sg/syntect/default.yaml | 5 +- .../testdata/sg/syntect/with-replicas.yaml | 5 +- .../testdata/sg/worker/default.yaml | 5 +- .../testdata/sg/worker/with-blobstore.yaml | 5 +- .../testdata/sg/worker/with-replicas.yaml | 5 +- internal/appliance/reconciler/worker.go | 22 +- internal/appliance/routes.go | 20 +- internal/appliance/selfupdate/BUILD.bazel | 31 + .../selfupdate/integrationtest/BUILD.bazel | 31 + .../integrationtest/integration_test.go | 150 +++ internal/appliance/selfupdate/selfupdate.go | 107 +++ .../appliance/selfupdate/selfupdate_test.go | 43 + internal/appliance/status.go | 128 +++ internal/appliance/status_test.go | 158 ++++ internal/appliance/upgrades/BUILD.bazel | 9 + internal/appliance/upgrades/util.go | 47 + internal/appliance/upgrades/util_test.go | 159 ++++ internal/appliance/versions.go | 32 +- internal/appliance/versions_test.go | 1 + .../web/static/css/bootstrap.min.css | 6 - internal/appliance/web/static/css/custom.css | 17 - internal/appliance/web/static/img/favicon.png | Bin 3473 -> 0 bytes .../web/static/script/bootstrap.bundle.min.js | 7 - .../appliance/web/static/script/htmx.min.js | 1 - internal/appliance/web/template/error.gohtml | 8 - .../appliance/web/template/landing.gohtml | 22 - internal/appliance/web/template/layout.gohtml | 17 - internal/appliance/web/template/setup.gohtml | 149 --- internal/conf/computed.go | 9 + internal/database/postgresdsn/postgresdsn.go | 1 + internal/releaseregistry/client.go | 9 + internal/releaseregistry/mocks/BUILD.bazel | 21 + internal/releaseregistry/mocks/mocks.go | 172 ++++ mockgen.test.yaml | 5 + wolfi-images/appliance-frontend.lock.json | 885 ++++++++++++++++++ wolfi-images/appliance-frontend.yaml | 40 + 234 files changed, 8985 insertions(+), 1844 deletions(-) create mode 100644 docker-images/appliance-frontend/BUILD.bazel create mode 100755 docker-images/appliance-frontend/image_test.yaml create mode 100755 docker-images/appliance-frontend/init.sh create mode 100755 docker-images/appliance-frontend/nginx.conf create mode 100644 internal/appliance/config/otel/agent.yaml delete mode 100644 internal/appliance/embed.go create mode 100755 internal/appliance/frontend/maintenance/image_test.yaml create mode 100755 internal/appliance/frontend/maintenance/maintenance.conf.template delete mode 100644 internal/appliance/frontend/maintenance/src/OperatorDebugBar.tsx delete mode 100644 internal/appliance/frontend/maintenance/src/debugBar.ts create mode 100644 internal/appliance/frontend/maintenance/src/state.ts create mode 100644 internal/appliance/healthchecker/BUILD.bazel create mode 100644 internal/appliance/healthchecker/health_checker.go create mode 100644 internal/appliance/healthchecker/health_checker_test.go create mode 100644 internal/appliance/healthchecker/probe.go create mode 100644 internal/appliance/json.go create mode 100644 internal/appliance/json_test.go create mode 100644 internal/appliance/k8senvtest/README.md create mode 100644 internal/appliance/k8senvtest/namespaces.go delete mode 100644 internal/appliance/maintenance/README.md delete mode 100644 internal/appliance/maintenance/backend/cmd/BUILD.bazel delete mode 100644 internal/appliance/maintenance/backend/cmd/main.go delete mode 100644 internal/appliance/maintenance/backend/operator/BUILD.bazel delete mode 100644 internal/appliance/maintenance/backend/operator/manage.go delete mode 100644 internal/appliance/maintenance/backend/operator/task.go delete mode 100644 internal/appliance/maintenance/demo.html delete mode 100644 internal/appliance/maintenance/helm/.helmignore delete mode 100644 internal/appliance/maintenance/helm/Chart.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/NOTES.txt delete mode 100644 internal/appliance/maintenance/helm/templates/api-deploy.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/api-secret.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/api-service.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/namespace.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/ui-deploy.yaml delete mode 100644 internal/appliance/maintenance/helm/templates/ui-service.yaml delete mode 100644 internal/appliance/maintenance/helm/values.yaml create mode 100644 internal/appliance/reconciler/otel_agent.go create mode 100644 internal/appliance/reconciler/otel_agent_test.go create mode 100644 internal/appliance/reconciler/secret_management.go create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/adopt-service.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeinsights-db-auth-secret.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeintel-db-auth-secret.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-pgsql-auth-secret.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-cache-secret.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-store-secret.yaml create mode 100644 internal/appliance/reconciler/testdata/golden-fixtures/otel-agent/default.yaml create mode 100644 internal/appliance/reconciler/testdata/sg/otel-agent/default.yaml create mode 100644 internal/appliance/selfupdate/BUILD.bazel create mode 100644 internal/appliance/selfupdate/integrationtest/BUILD.bazel create mode 100644 internal/appliance/selfupdate/integrationtest/integration_test.go create mode 100644 internal/appliance/selfupdate/selfupdate.go create mode 100644 internal/appliance/selfupdate/selfupdate_test.go create mode 100644 internal/appliance/status.go create mode 100644 internal/appliance/status_test.go delete mode 100644 internal/appliance/web/static/css/bootstrap.min.css delete mode 100644 internal/appliance/web/static/css/custom.css delete mode 100644 internal/appliance/web/static/img/favicon.png delete mode 100644 internal/appliance/web/static/script/bootstrap.bundle.min.js delete mode 100644 internal/appliance/web/static/script/htmx.min.js delete mode 100644 internal/appliance/web/template/error.gohtml delete mode 100644 internal/appliance/web/template/landing.gohtml delete mode 100644 internal/appliance/web/template/layout.gohtml delete mode 100644 internal/appliance/web/template/setup.gohtml create mode 100644 internal/releaseregistry/mocks/BUILD.bazel create mode 100644 internal/releaseregistry/mocks/mocks.go create mode 100755 wolfi-images/appliance-frontend.lock.json create mode 100755 wolfi-images/appliance-frontend.yaml diff --git a/client/web/dev/utils/create-js-context.ts b/client/web/dev/utils/create-js-context.ts index d364ee8576d18..06e749fbb7015 100644 --- a/client/web/dev/utils/create-js-context.ts +++ b/client/web/dev/utils/create-js-context.ts @@ -35,6 +35,7 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st accessTokensExpirationDaysOptions: [7, 14, 30, 60, 90], allowSignup: true, batchChangesEnabled: true, + applianceManaged: false, batchChangesDisableWebhooksWarning: false, batchChangesWebhookLogsEnabled: true, executorsEnabled: false, diff --git a/client/web/src/enterprise/site-admin/SiteAdminSidebar.story.tsx b/client/web/src/enterprise/site-admin/SiteAdminSidebar.story.tsx index 5274dd0f68344..0d83fc1494054 100644 --- a/client/web/src/enterprise/site-admin/SiteAdminSidebar.story.tsx +++ b/client/web/src/enterprise/site-admin/SiteAdminSidebar.story.tsx @@ -38,6 +38,7 @@ export const AdminSidebarItems: StoryFn = () => ( batchChangesExecutionEnabled={true} batchChangesWebhookLogsEnabled={true} codeInsightsEnabled={true} + applianceManaged={false} endUserOnboardingEnabled={false} /> ( batchChangesExecutionEnabled={true} batchChangesWebhookLogsEnabled={true} codeInsightsEnabled={true} + applianceManaged={false} endUserOnboardingEnabled={false} /> ( batchChangesExecutionEnabled={false} batchChangesWebhookLogsEnabled={false} codeInsightsEnabled={true} + applianceManaged={false} endUserOnboardingEnabled={false} /> ( batchChangesExecutionEnabled={true} batchChangesWebhookLogsEnabled={true} codeInsightsEnabled={false} + applianceManaged={false} endUserOnboardingEnabled={false} /> diff --git a/client/web/src/integration/jscontext.ts b/client/web/src/integration/jscontext.ts index 07c709ad430cb..472aef8e2d5ef 100644 --- a/client/web/src/integration/jscontext.ts +++ b/client/web/src/integration/jscontext.ts @@ -27,6 +27,7 @@ export const createJsContext = ({ sourcegraphBaseUrl }: { sourcegraphBaseUrl: st accessTokensExpirationDaysOptions: [7, 30, 60, 90], allowSignup: false, batchChangesEnabled: true, + applianceManaged: false, batchChangesDisableWebhooksWarning: false, batchChangesWebhookLogsEnabled: true, codeInsightsEnabled: true, diff --git a/client/web/src/jscontext.ts b/client/web/src/jscontext.ts index 7356b3622fe93..dedc427617c6d 100644 --- a/client/web/src/jscontext.ts +++ b/client/web/src/jscontext.ts @@ -196,6 +196,11 @@ export interface SourcegraphContext extends Pick, 'e batchChangesWebhookLogsEnabled: boolean + /** + * Whether this sourcegraph instance is managed by Appliance + */ + applianceManaged: boolean + /** * Whether Cody is enabled on this instance. Check * {@link SourcegraphContext.codyEnabledForCurrentUser} to see whether Cody is enabled for the diff --git a/client/web/src/routes.tsx b/client/web/src/routes.tsx index 31c6debd739f8..76ebc06d11c01 100644 --- a/client/web/src/routes.tsx +++ b/client/web/src/routes.tsx @@ -268,6 +268,7 @@ export const routes: RouteObject[] = [ sideBarGroups={props.siteAdminSideBarGroups} overviewComponents={props.siteAdminOverviewComponents} codeInsightsEnabled={window.context.codeInsightsEnabled} + applianceManaged={window.context.applianceManaged} telemetryRecorder={props.platformContext.telemetryRecorder} /> )} diff --git a/client/web/src/site-admin/SiteAdminArea.tsx b/client/web/src/site-admin/SiteAdminArea.tsx index 46d5510e48aab..2b024468d6dff 100644 --- a/client/web/src/site-admin/SiteAdminArea.tsx +++ b/client/web/src/site-admin/SiteAdminArea.tsx @@ -59,6 +59,7 @@ export interface SiteAdminAreaRouteContext overviewComponents: readonly React.ComponentType>[] codeInsightsEnabled: boolean + applianceManaged: boolean endUserOnboardingEnabled: boolean } @@ -77,6 +78,7 @@ interface SiteAdminAreaProps authenticatedUser: AuthenticatedUser isSourcegraphDotCom: boolean codeInsightsEnabled: boolean + applianceManaged: boolean } const sourcegraphOperatorSiteAdminMaintenanceBlockItems = new Set([ @@ -142,6 +144,7 @@ const AuthenticatedSiteAdminArea: React.FunctionComponent
diff --git a/client/web/src/site-admin/SiteAdminSidebar.tsx b/client/web/src/site-admin/SiteAdminSidebar.tsx index 8a5a45ad1c672..0e7b00800b2a3 100644 --- a/client/web/src/site-admin/SiteAdminSidebar.tsx +++ b/client/web/src/site-admin/SiteAdminSidebar.tsx @@ -15,6 +15,7 @@ export interface SiteAdminSideBarGroupContext extends BatchChangesProps { isSourcegraphDotCom: boolean codeInsightsEnabled: boolean endUserOnboardingEnabled: boolean + applianceManaged: boolean } export interface SiteAdminSideBarGroup extends NavGroupDescriptor {} diff --git a/client/web/src/site-admin/sidebaritems.ts b/client/web/src/site-admin/sidebaritems.ts index 5fe66b6cde895..02c4873b0104f 100644 --- a/client/web/src/site-admin/sidebaritems.ts +++ b/client/web/src/site-admin/sidebaritems.ts @@ -135,6 +135,12 @@ const maintenanceGroup: SiteAdminSideBarGroup = { { label: maintenanceGroupUpdatesItemLabel, to: '/site-admin/updates', + condition: ({ applianceManaged }) => !applianceManaged, + }, + { + label: maintenanceGroupUpdatesItemLabel, + to: '/appliance/updates', + condition: ({ applianceManaged }) => applianceManaged, }, { label: 'Documentation', diff --git a/cmd/appliance/shared/BUILD.bazel b/cmd/appliance/shared/BUILD.bazel index 0d27f3c96dea0..4370d9046996e 100644 --- a/cmd/appliance/shared/BUILD.bazel +++ b/cmd/appliance/shared/BUILD.bazel @@ -11,7 +11,9 @@ go_library( visibility = ["//visibility:public"], deps = [ "//internal/appliance", + "//internal/appliance/healthchecker", "//internal/appliance/reconciler", + "//internal/appliance/selfupdate", "//internal/appliance/v1:appliance", "//internal/debugserver", "//internal/env", @@ -23,6 +25,7 @@ go_library( "//lib/errors", "@com_github_sourcegraph_log//:log", "@com_github_sourcegraph_log_logr//:logr", + "@io_k8s_apimachinery//pkg/types", "@io_k8s_client_go//rest", "@io_k8s_client_go//tools/clientcmd", "@io_k8s_client_go//util/homedir", diff --git a/cmd/appliance/shared/config.go b/cmd/appliance/shared/config.go index 5189c68a14938..2d18691ddc043 100644 --- a/cmd/appliance/shared/config.go +++ b/cmd/appliance/shared/config.go @@ -16,13 +16,15 @@ import ( type Config struct { env.BaseConfig - k8sConfig *rest.Config - metrics metricsConfig - grpc grpcConfig - http httpConfig - namespace string - relregEndpoint string - applianceVersion string + k8sConfig *rest.Config + metrics metricsConfig + grpc grpcConfig + http httpConfig + namespace string + relregEndpoint string + applianceVersion string + selfDeploymentName string + noResourceRestrictions string } func (c *Config) Load() { @@ -43,10 +45,12 @@ func (c *Config) Load() { c.metrics.addr = c.Get("APPLIANCE_METRICS_ADDR", ":8734", "Appliance metrics server address.") c.metrics.secure = c.GetBool("APPLIANCE_METRICS_SECURE", "false", "Appliance metrics server uses https.") c.grpc.addr = c.Get("APPLIANCE_GRPC_ADDR", ":9000", "Appliance gRPC address.") - c.http.addr = c.Get("APPLIANCE_HTTP_ADDR", ":8080", "Appliance http address.") + c.http.addr = c.Get("APPLIANCE_HTTP_ADDR", ":8888", "Appliance http address.") c.namespace = c.Get("APPLIANCE_NAMESPACE", "default", "Namespace to monitor.") c.applianceVersion = c.Get("APPLIANCE_VERSION", version.Version(), "Version tag for the running appliance.") + c.selfDeploymentName = c.Get("APPLIANCE_DEPLOYMENT_NAME", "", "Own deployment name for self-update. Default is to disable self-update.") c.relregEndpoint = c.Get("RELEASE_REGISTRY_ENDPOINT", releaseregistry.Endpoint, "Release registry endpoint.") + c.noResourceRestrictions = c.Get("APPLIANCE_NO_RESOURCE_RESTRICTIONS", "false", "Remove all resource requests and limits from deployed resources. Only recommended for local development.") } func (c *Config) Validate() error { diff --git a/cmd/appliance/shared/shared.go b/cmd/appliance/shared/shared.go index 2c20695354e91..1aae6c7cd5cb9 100644 --- a/cmd/appliance/shared/shared.go +++ b/cmd/appliance/shared/shared.go @@ -6,11 +6,13 @@ import ( "net/http" "os" "os/signal" + "strconv" "syscall" "time" "golang.org/x/sync/errgroup" "google.golang.org/grpc" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,8 +20,11 @@ import ( "github.com/sourcegraph/log" sglogr "github.com/sourcegraph/log/logr" + "github.com/sourcegraph/sourcegraph/internal/appliance" + "github.com/sourcegraph/sourcegraph/internal/appliance/healthchecker" "github.com/sourcegraph/sourcegraph/internal/appliance/reconciler" + "github.com/sourcegraph/sourcegraph/internal/appliance/selfupdate" pb "github.com/sourcegraph/sourcegraph/internal/appliance/v1" "github.com/sourcegraph/sourcegraph/internal/grpc/defaults" "github.com/sourcegraph/sourcegraph/internal/observation" @@ -44,7 +49,14 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi relregClient := releaseregistry.NewClient(config.relregEndpoint) - app, err := appliance.NewAppliance(k8sClient, relregClient, config.applianceVersion, config.namespace, logger) + noResourceRestrictions := false + noResourceRestrictions, err = strconv.ParseBool(config.noResourceRestrictions) + if err != nil { + logger.Error("parsing APPLIANCE_NO_RESOURCE_RESTRICTIONS as bool", log.Error(err)) + return err + } + + app, err := appliance.NewAppliance(k8sClient, relregClient, config.applianceVersion, config.namespace, noResourceRestrictions, logger) if err != nil { logger.Error("failed to create appliance", log.Error(err)) return err @@ -67,10 +79,13 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi return err } + beginHealthCheckLoop := make(chan struct{}) + if err = (&reconciler.Reconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("sourcegraph-appliance"), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("sourcegraph-appliance"), + BeginHealthCheckLoop: beginHealthCheckLoop, }).SetupWithManager(mgr); err != nil { logger.Error("unable to create the appliance controller", log.Error(err)) return err @@ -92,6 +107,26 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi grpcServer := makeGRPCServer(logger, app) + selfUpdater := &selfupdate.SelfUpdate{ + Interval: time.Hour, + Logger: logger.Scoped("SelfUpdate"), + K8sClient: k8sClient, + RelregClient: relregClient, + DeploymentNames: config.selfDeploymentName, + Namespace: config.namespace, + } + + probe := &healthchecker.PodProbe{K8sClient: k8sClient} + healthChecker := &healthchecker.HealthChecker{ + Probe: probe, + K8sClient: k8sClient, + Logger: logger.Scoped("HealthChecker"), + + ServiceName: types.NamespacedName{Name: "sourcegraph-frontend", Namespace: config.namespace}, + Interval: time.Minute, + Graceperiod: time.Minute, + } + g, ctx := errgroup.WithContext(ctx) ctx = shutdownOnSignal(ctx) @@ -119,6 +154,18 @@ func Start(ctx context.Context, observationCtx *observation.Context, ready servi } return nil }) + g.Go(func() error { + if err := healthChecker.ManageIngressFacingService(ctx, beginHealthCheckLoop, "app=sourcegraph-frontend", config.namespace); err != nil { + logger.Error("problem running HealthChecker", log.Error(err)) + return err + } + return nil + }) + if config.selfDeploymentName != "" { + g.Go(func() error { + return selfUpdater.Loop(ctx) + }) + } g.Go(func() error { <-ctx.Done() grpcServer.GracefulStop() diff --git a/cmd/frontend/internal/app/jscontext/jscontext.go b/cmd/frontend/internal/app/jscontext/jscontext.go index a7d1e40977f72..b63991b9e2634 100644 --- a/cmd/frontend/internal/app/jscontext/jscontext.go +++ b/cmd/frontend/internal/app/jscontext/jscontext.go @@ -233,6 +233,7 @@ type JSContext struct { CodeIntelRankingDocumentReferenceCountsEnabled bool `json:"codeIntelRankingDocumentReferenceCountsEnabled"` CodeInsightsEnabled bool `json:"codeInsightsEnabled"` + ApplianceManaged bool `json:"applianceManaged"` CodeIntelligenceEnabled bool `json:"codeIntelligenceEnabled"` SearchContextsEnabled bool `json:"searchContextsEnabled"` NotebooksEnabled bool `json:"notebooksEnabled"` @@ -436,6 +437,7 @@ func NewJSContextFromRequest(req *http.Request, db database.DB) JSContext { CodyRequiresVerifiedEmail: siteResolver.RequiresVerifiedEmailForCody(ctx), CodeSearchEnabledOnInstance: codeSearchLicensed, + ApplianceManaged: conf.IsApplianceManaged(), ExecutorsEnabled: conf.ExecutorsEnabled(), CodeIntelAutoIndexingEnabled: conf.CodeIntelAutoIndexingEnabled(), diff --git a/deps.bzl b/deps.bzl index 5d4dbc8b7e0ec..e66ce884de620 100644 --- a/deps.bzl +++ b/deps.bzl @@ -6419,13 +6419,6 @@ def go_dependencies(): sum = "h1:dH55ru2OQOIAKjZi5wwXjNnSfN0oXLFYkMQy908s+tU=", version = "v0.2.0", ) - go_repository( - name = "com_github_wagslane_go_password_validator", - build_file_proto_mode = "disable_global", - importpath = "github.com/wagslane/go-password-validator", - sum = "h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=", - version = "v0.3.0", - ) go_repository( name = "com_github_wk8_go_ordered_map_v2", build_file_proto_mode = "disable_global", diff --git a/dev/BUILD.bazel b/dev/BUILD.bazel index b2362775c275b..86c1b161c5e4c 100644 --- a/dev/BUILD.bazel +++ b/dev/BUILD.bazel @@ -127,5 +127,6 @@ write_source_files( "//cmd/enterprise-portal/internal/subscriptionsservice:generate_mocks", "//dev/sg/internal/analytics:generate_mocks", "//cmd/symbols/internal/fetcher:generate_mocks", + "//internal/releaseregistry/mocks:generate_mocks", ], ) diff --git a/docker-images/appliance-frontend/BUILD.bazel b/docker-images/appliance-frontend/BUILD.bazel new file mode 100644 index 0000000000000..63f133cd49dac --- /dev/null +++ b/docker-images/appliance-frontend/BUILD.bazel @@ -0,0 +1,71 @@ +load("//dev:oci_defs.bzl", "image_repository", "oci_image", "oci_push", "oci_tarball") +load("@rules_pkg//:pkg.bzl", "pkg_tar") +load("@container_structure_test//:defs.bzl", "container_structure_test") +load("//wolfi-images:defs.bzl", "wolfi_base") + +filegroup( + name = "config", + srcs = ["nginx.conf"], +) + +filegroup( + name = "init_script", + srcs = ["init.sh"], +) + +pkg_tar( + name = "tar_config", + srcs = [":config"], + package_dir = "/etc/nginx", +) + +pkg_tar( + name = "tar_init_script", + srcs = [":init_script"], + package_dir = "/", +) + +oci_image( + name = "image", + base = ":base_image", + entrypoint = [ + "/init.sh", + "nginx", + "-g", + "daemon off;", + ], + tars = [ + ":tar_init_script", + ":tar_config", + "//internal/appliance/frontend/maintenance:tar_config", + "//internal/appliance/frontend/maintenance:tar_frontend", + ], + user = "sourcegraph", +) + +oci_tarball( + name = "image_tarball", + image = ":image", + repo_tags = ["appliance-frontend:candidate"], +) + +container_structure_test( + name = "image_test", + timeout = "short", + configs = ["image_test.yaml"], + driver = "docker", + image = ":image", + tags = [ + "exclusive", + "requires-network", + TAG_INFRA_DEVINFRA, + ], +) + +oci_push( + name = "candidate_push", + image = ":image", + repository = image_repository("appliance-frontend"), +) + +wolfi_base() diff --git a/docker-images/appliance-frontend/image_test.yaml b/docker-images/appliance-frontend/image_test.yaml new file mode 100755 index 0000000000000..5135b24d3b25c --- /dev/null +++ b/docker-images/appliance-frontend/image_test.yaml @@ -0,0 +1,14 @@ +schemaVersion: "2.0.0" + +commandTests: + - name: "nginx is runnable" + command: "nginx" + args: + - -v + + - name: "not running as root" + command: "/usr/bin/id" + args: + - -u + excludedOutput: ["^0"] + exitCode: 0 diff --git a/docker-images/appliance-frontend/init.sh b/docker-images/appliance-frontend/init.sh new file mode 100755 index 0000000000000..21a3f7faca7f3 --- /dev/null +++ b/docker-images/appliance-frontend/init.sh @@ -0,0 +1,18 @@ +#!/bin/sh +template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}" +suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}" +output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}" +filter="${NGINX_ENVSUBST_FILTER:-}" +# shellcheck disable=SC2046 +defined_envs=$(printf "\${%s} " $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" $output_path" + envsubst "$defined_envs" <"$template" >"$output_path" +done + +exec "$@" diff --git a/docker-images/appliance-frontend/nginx.conf b/docker-images/appliance-frontend/nginx.conf new file mode 100755 index 0000000000000..92e761ef2963b --- /dev/null +++ b/docker-images/appliance-frontend/nginx.conf @@ -0,0 +1,16 @@ +worker_processes 1; +error_log stderr warn; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + access_log off; + sendfile on; + keepalive_timeout 65; + + include conf.d/*.conf; +} diff --git a/go.mod b/go.mod index 5ba74e597c390..ebfa87be670ad 100644 --- a/go.mod +++ b/go.mod @@ -258,6 +258,7 @@ require ( connectrpc.com/connect v1.16.2 connectrpc.com/grpcreflect v1.2.0 connectrpc.com/otelconnect v0.7.0 + dario.cat/mergo v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 @@ -275,7 +276,6 @@ require ( github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redsync/redsync/v4 v4.13.0 - github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-containerregistry v0.16.1 github.com/google/go-github/v48 v48.2.0 github.com/google/go-github/v55 v55.0.0 @@ -320,7 +320,6 @@ require ( github.com/sourcegraph/sourcegraph/monitoring v0.0.0-00010101000000-000000000000 github.com/vektah/gqlparser/v2 v2.4.5 github.com/vvakame/gcplogurl v0.2.0 - github.com/wagslane/go-password-validator v0.3.0 go.opentelemetry.io/collector/config/confighttp v0.103.0 go.opentelemetry.io/collector/config/configtelemetry v0.103.0 go.opentelemetry.io/collector/config/configtls v0.103.0 @@ -342,7 +341,6 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/longrunning v0.5.6 // indirect cloud.google.com/go/trace v1.10.6 // indirect - dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect @@ -390,6 +388,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.20.1 // indirect diff --git a/go.sum b/go.sum index af4936282054b..610ce4df8a173 100644 --- a/go.sum +++ b/go.sum @@ -2432,8 +2432,6 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vvakame/gcplogurl v0.2.0 h1:dH55ru2OQOIAKjZi5wwXjNnSfN0oXLFYkMQy908s+tU= github.com/vvakame/gcplogurl v0.2.0/go.mod h1:CFjKFlur6M+/2DoGZL67O1FqZxB42jiqCyl4cXAmjOU= -github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= -github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= 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/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= diff --git a/internal/appliance/BUILD.bazel b/internal/appliance/BUILD.bazel index f2de009b4a1f4..10fe43ae97296 100644 --- a/internal/appliance/BUILD.bazel +++ b/internal/appliance/BUILD.bazel @@ -6,24 +6,14 @@ go_library( srcs = [ "appliance.go", "auth.go", - "embed.go", "errors.go", "grpc.go", "html.go", + "json.go", "routes.go", + "status.go", "versions.go", ], - embedsrcs = [ - "web/static/img/favicon.png", - "web/static/script/htmx.min.js", - "web/template/setup.gohtml", - "web/static/css/bootstrap.min.css", - "web/static/css/custom.css", - "web/static/script/bootstrap.bundle.min.js", - "web/template/layout.gohtml", - "web/template/landing.gohtml", - "web/template/error.gohtml", - ], importpath = "github.com/sourcegraph/sourcegraph/internal/appliance", visibility = ["//:__subpackages__"], deps = [ @@ -32,12 +22,12 @@ go_library( "//internal/releaseregistry", "//lib/errors", "//lib/pointers", - "@com_github_golang_jwt_jwt_v5//:jwt", + "@cat_dario_mergo//:mergo", "@com_github_gorilla_mux//:mux", "@com_github_life4_genesis//slices", "@com_github_masterminds_semver_v3//:semver", "@com_github_sourcegraph_log//:log", - "@com_github_wagslane_go_password_validator//:go-password-validator", + "@io_k8s_api//apps/v1:apps", "@io_k8s_api//core/v1:core", "@io_k8s_apimachinery//pkg/api/errors", "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", @@ -57,12 +47,17 @@ go_test( name = "appliance_test", srcs = [ "auth_test.go", + "json_test.go", + "status_test.go", "versions_test.go", ], embed = [":appliance"], deps = [ - "@com_github_golang_jwt_jwt_v5//:jwt", + "@com_github_google_go_cmp//cmp", "@com_github_sourcegraph_log//:log", "@com_github_stretchr_testify//require", + "@io_k8s_api//apps/v1:apps", + "@io_k8s_api//core/v1:core", + "@io_k8s_sigs_controller_runtime//pkg/client", ], ) diff --git a/internal/appliance/appliance.go b/internal/appliance/appliance.go index 56587d2f49d0a..1c92cc0ac93a6 100644 --- a/internal/appliance/appliance.go +++ b/internal/appliance/appliance.go @@ -2,17 +2,18 @@ package appliance import ( "context" - "crypto/rand" + "dario.cat/mergo" "golang.org/x/crypto/bcrypt" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/internal/appliance/config" pb "github.com/sourcegraph/sourcegraph/internal/appliance/v1" "github.com/sourcegraph/sourcegraph/internal/releaseregistry" @@ -21,15 +22,15 @@ import ( ) type Appliance struct { - jwtSecret []byte adminPasswordBcrypt []byte client client.Client namespace string - status Status + status config.Status sourcegraph *config.Sourcegraph releaseRegistryClient *releaseregistry.Client latestSupportedVersion string + noResourceRestrictions bool logger log.Logger // Embed the UnimplementedApplianceServiceServer structs to ensure forwards compatibility (if the service is @@ -38,31 +39,20 @@ type Appliance struct { pb.UnimplementedApplianceServiceServer } -// Status is a Stage that an Appliance can be in. -type Status string - const ( - StatusUnknown Status = "unknown" - StatusSetup Status = "setup" - StatusInstalling Status = "installing" - // Secret and key names dataSecretName = "appliance-data" - dataSecretJWTSigningKeyKey = "jwt-signing-key" dataSecretEncryptedPasswordKey = "encrypted-admin-password" initialPasswordSecretName = "appliance-password" initialPasswordSecretPasswordKey = "password" ) -func (s Status) String() string { - return string(s) -} - func NewAppliance( client client.Client, relregClient *releaseregistry.Client, latestSupportedVersion string, namespace string, + noResourceRestrictions bool, logger log.Logger, ) (*Appliance, error) { app := &Appliance{ @@ -70,7 +60,8 @@ func NewAppliance( releaseRegistryClient: relregClient, latestSupportedVersion: latestSupportedVersion, namespace: namespace, - status: StatusSetup, + status: config.StatusInstall, + noResourceRestrictions: noResourceRestrictions, sourcegraph: &config.Sourcegraph{}, logger: logger, } @@ -109,13 +100,6 @@ func (a *Appliance) ensureBackingSecretKeysExist(ctx context.Context, secret *co if secret.Data == nil { secret.Data = map[string][]byte{} } - if _, ok := secret.Data[dataSecretJWTSigningKeyKey]; !ok { - jwtSigningKey, err := genRandomBytes(32) - if err != nil { - return err - } - secret.Data[dataSecretJWTSigningKeyKey] = jwtSigningKey - } if _, ok := secret.Data[dataSecretEncryptedPasswordKey]; !ok { // Get admin-supplied password from separate secret, then delete it @@ -150,88 +134,106 @@ func (a *Appliance) ensureBackingSecretKeysExist(ctx context.Context, secret *co } func (a *Appliance) loadValuesFromSecret(secret *corev1.Secret) { - a.jwtSecret = secret.Data[dataSecretJWTSigningKeyKey] a.adminPasswordBcrypt = secret.Data[dataSecretEncryptedPasswordKey] } -func genRandomBytes(length int) ([]byte, error) { - randomBytes := make([]byte, length) - bytesRead, err := rand.Read(randomBytes) - if err != nil { - return nil, errors.Wrap(err, "reading random bytes") - } - if bytesRead != length { - return nil, errors.Newf("expected to read %d random bytes, got %d", length, bytesRead) - } - return randomBytes, nil -} - func (a *Appliance) GetCurrentVersion(ctx context.Context) string { return a.sourcegraph.Status.CurrentVersion } -func (a *Appliance) GetCurrentStatus(ctx context.Context) Status { +func (a *Appliance) GetCurrentStatus(ctx context.Context) config.Status { return a.status } -func (a *Appliance) CreateConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) { - spec, err := yaml.Marshal(a.sourcegraph) - if err != nil { - return nil, err - } +func (a *Appliance) reconcileConfigMap(ctx context.Context, configMap *corev1.ConfigMap) error { + existingCfgMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace} + existingCfgMap := &corev1.ConfigMap{} + if err := a.client.Get(ctx, existingCfgMapName, existingCfgMap); err != nil { + // Create the ConfigMap if not found + if apierrors.IsNotFound(err) { + spec, err := yaml.Marshal(a.sourcegraph) + if err != nil { + return errors.Wrap(err, "failed to marshal configmap yaml") + } + + cfgMap := &corev1.ConfigMap{} + cfgMap.Name = config.ConfigmapName + cfgMap.Namespace = a.namespace - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: a.namespace, - Labels: map[string]string{ + cfgMap.Labels = map[string]string{ "deploy": "sourcegraph", - }, - Annotations: map[string]string{ + } + + cfgMap.Annotations = map[string]string{ // required annotation for our controller filter. config.AnnotationKeyManaged: "true", - }, - }, - Immutable: pointers.Ptr(false), - Data: map[string]string{ - "spec": string(spec), - }, + config.AnnotationKeyStatus: string(config.StatusUnknown), + config.AnnotationConditions: "", + } + + if configMap.ObjectMeta.Annotations != nil { + cfgMap.ObjectMeta.Annotations = configMap.ObjectMeta.Annotations + } + + cfgMap.Immutable = pointers.Ptr(false) + cfgMap.Data = map[string]string{"spec": string(spec)} + + return a.client.Create(ctx, cfgMap) + } + + return errors.Wrap(err, "getting configmap") } - if err := a.client.Create(ctx, configMap); err != nil { - return nil, err + // The configmap already exists, update with any changed values + if err := mergo.Merge(existingCfgMap, configMap, mergo.WithOverride); err != nil { + return errors.Wrap(err, "merging configmaps") } - return configMap, nil + return a.client.Update(ctx, existingCfgMap) } -func (a *Appliance) GetConfigMap(ctx context.Context, name string) (*corev1.ConfigMap, error) { - var applianceSpec corev1.ConfigMap - err := a.client.Get(ctx, types.NamespacedName{Name: name, Namespace: a.namespace}, &applianceSpec) - if apierrors.IsNotFound(err) { - return nil, nil - } else if err != nil { - return nil, err +// isSourcegraphFrontendReady is a "health check" that is used to be able to know when our backing sourcegraph +// deployment is ready. This is a "quick and dirty" function and should be replaced with a more comprehensive +// health check in the very near future. +func (a *Appliance) isSourcegraphFrontendReady(ctx context.Context) (bool, error) { + frontendDeploymentName := types.NamespacedName{Name: "sourcegraph-frontend", Namespace: a.namespace} + frontendDeployment := &appsv1.Deployment{} + if err := a.client.Get(ctx, frontendDeploymentName, frontendDeployment); err != nil { + // If the frontend deployment is not found, we can assume it's not ready + if apierrors.IsNotFound(err) { + return false, nil + } + return false, errors.Wrap(err, "fetching frontend deployment") + } + + return IsObjectReady(frontendDeployment) +} + +func (a *Appliance) getStatus(ctx context.Context) (config.Status, error) { + configMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace} + configMap := &corev1.ConfigMap{} + if err := a.client.Get(ctx, configMapName, configMap); err != nil { + if apierrors.IsNotFound(err) { + return config.StatusUnknown, nil + } + return config.StatusUnknown, err } - return &applianceSpec, nil + return config.Status(configMap.ObjectMeta.Annotations[config.AnnotationKeyStatus]), nil } -func (a *Appliance) shouldSetupRun(ctx context.Context) (bool, error) { - cfgMap, err := a.GetConfigMap(ctx, "sourcegraph-appliance") - switch { - case err != nil: - return false, err - case a.status == StatusInstalling: - // configMap does not exist but is being created - return false, nil - case cfgMap == nil: - // configMap does not exist - return true, nil - case cfgMap.Annotations[config.AnnotationKeyManaged] == "false": - // appliance is not managed - return false, nil - default: - return true, nil +func (a *Appliance) setStatus(ctx context.Context, status config.Status) error { + configMapName := types.NamespacedName{Name: config.ConfigmapName, Namespace: a.namespace} + configMap := &corev1.ConfigMap{} + if err := a.client.Get(ctx, configMapName, configMap); err != nil { + return err } + + configMap.Annotations[config.AnnotationKeyStatus] = string(status) + err := a.client.Update(ctx, configMap) + if err != nil { + return errors.Wrap(err, "failed set status") + } + + return nil } diff --git a/internal/appliance/auth.go b/internal/appliance/auth.go index 7008152734c88..d92ee597e7d91 100644 --- a/internal/appliance/auth.go +++ b/internal/appliance/auth.go @@ -2,74 +2,22 @@ package appliance import ( "net/http" - "time" - "github.com/golang-jwt/jwt/v5" - - "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/lib/errors" + "golang.org/x/crypto/bcrypt" ) const ( - authCookieName = "applianceAuth" - jwtClaimsValidUntilKey = "valid-until" + authHeaderName = "admin-password" ) -func (a *Appliance) CheckAuthorization(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - authCookie, err := req.Cookie(authCookieName) - if err != nil { - a.authRedirect(w, req, err) +func (a *Appliance) checkAuthorization(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userPass := r.Header.Get(authHeaderName) + if err := bcrypt.CompareHashAndPassword(a.adminPasswordBcrypt, []byte(userPass)); err != nil { + a.invalidAdminPasswordResponse(w, r) return } - token, err := jwt.Parse(authCookie.Value, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return a.jwtSecret, nil - }) - if err != nil { - a.authRedirect(w, req, err) - return - } - if !token.Valid { - a.authRedirect(w, req, errors.New("JWT is not valid")) - return - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - a.authRedirect(w, req, errors.New("JWT Claims are not a MapClaims")) - return - } - validUntilStr, ok := claims[jwtClaimsValidUntilKey].(string) - if !ok { - err := errors.Newf("JWT does not contain a string field '%s'", jwtClaimsValidUntilKey) - a.authRedirect(w, req, err) - return - } - validUntil, err := time.Parse(time.RFC3339, validUntilStr) - if err != nil { - a.authRedirect(w, req, errors.Wrapf(err, "parsing %s field on JWT claims", jwtClaimsValidUntilKey)) - return - } - if time.Now().After(validUntil) { - a.authRedirect(w, req, errors.Newf("JWT expired: %s", validUntil.String())) - return - } - - next.ServeHTTP(w, req) + next.ServeHTTP(w, r) }) } - -func (a *Appliance) authRedirect(w http.ResponseWriter, req *http.Request, err error) { - a.logger.Info("admin authorization failed", log.Error(err)) - deletedCookie := &http.Cookie{ - Name: authCookieName, - Value: "", - Expires: time.Unix(0, 0), - } - http.SetCookie(w, deletedCookie) - http.Redirect(w, req, "/appliance/login", http.StatusFound) -} diff --git a/internal/appliance/auth_test.go b/internal/appliance/auth_test.go index ddd94d29e04c1..a76a37fd93f06 100644 --- a/internal/appliance/auth_test.go +++ b/internal/appliance/auth_test.go @@ -4,122 +4,64 @@ import ( "net/http" "net/http/httptest" "testing" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/stretchr/testify/require" - - "github.com/sourcegraph/log" ) -var appliance = &Appliance{ - jwtSecret: []byte("a-jwt-secret"), - logger: log.NoOp(), -} - -func TestCheckAuthorization_CallsNextHandlerWhenValidJWTSupplied(t *testing.T) { - validUntil := time.Now().Add(time.Hour).UTC() - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339), - }) - tokenStr, err := token.SignedString(appliance.jwtSecret) - require.NoError(t, err) - - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: authCookieName, - Value: tokenStr, - Expires: validUntil, - }) - - handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusAccepted) - }) - respSpy := httptest.NewRecorder() - appliance.CheckAuthorization(handler).ServeHTTP(respSpy, req) - - require.Equal(t, http.StatusAccepted, respSpy.Code) -} - -func TestCheckAuthorization_RedirectsToErrorPageWhenNoCookieSupplied(t *testing.T) { - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - assertDirectAndHandlerNotCalled(t, req) -} - -func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsInvalidJWT(t *testing.T) { - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: authCookieName, - Value: "not-a-jwt", - Expires: time.Now().Add(time.Hour), - }) - assertDirectAndHandlerNotCalled(t, req) -} - -func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithIncorrectSignature(t *testing.T) { - validUntil := time.Now().Add(time.Hour).UTC() - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339), - }) - tokenStr, err := token.SignedString([]byte("wrong-key!")) - - require.NoError(t, err) - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: authCookieName, - Value: tokenStr, - Expires: validUntil, - }) - assertDirectAndHandlerNotCalled(t, req) -} - -func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithMalformedClaims(t *testing.T) { - validUntil := time.Now().Add(time.Hour).UTC() - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "wrong-key": validUntil.Format(time.RFC3339), - }) - tokenStr, err := token.SignedString(appliance.jwtSecret) - - require.NoError(t, err) - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: authCookieName, - Value: tokenStr, - Expires: validUntil, - }) - assertDirectAndHandlerNotCalled(t, req) -} - -func TestCheckAuthorization_RedirectsToErrorPageWhenCookieContainsJWTWithExpiredValidity(t *testing.T) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - jwtClaimsValidUntilKey: time.Now().Add(-1 * time.Hour).Format(time.RFC3339), - }) - tokenStr, err := token.SignedString(appliance.jwtSecret) - - require.NoError(t, err) - req, err := http.NewRequest("GET", "example.com", nil) - require.NoError(t, err) - req.AddCookie(&http.Cookie{ - Name: authCookieName, - Value: tokenStr, - Expires: time.Now().Add(time.Hour), - }) - assertDirectAndHandlerNotCalled(t, req) -} - -func assertDirectAndHandlerNotCalled(t *testing.T, req *http.Request) { - handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - require.Fail(t, "next handler should not be called") - }) - respSpy := httptest.NewRecorder() - appliance.CheckAuthorization(handler).ServeHTTP(respSpy, req) - - require.Equal(t, http.StatusFound, respSpy.Code) +func TestCheckAuthorization(t *testing.T) { + // Create a mock Appliance + mockAppliance := &Appliance{ + adminPasswordBcrypt: []byte("$2y$10$o2gHR6vUX7XPQj8tjUfi/e0zel.kpgvdTdSUkQthO9hTYooDUuoay"), // bcrypt hash for "password123" + } + + tests := []struct { + name string + password string + expectedStatus int + shouldCallNextHandler bool + }{ + { + name: "Valid password", + password: "password123", + expectedStatus: http.StatusOK, + shouldCallNextHandler: true, + }, + { + name: "Invalid password", + password: "wrongpassword", + expectedStatus: http.StatusUnauthorized, + shouldCallNextHandler: false, + }, + { + name: "Empty password", + password: "", + expectedStatus: http.StatusUnauthorized, + shouldCallNextHandler: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nextHandlerCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextHandlerCalled = true + if !tt.shouldCallNextHandler { + t.Error("Next handler should not be called after a 403") + } + }) + + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set(authHeaderName, tt.password) + rr := httptest.NewRecorder() + + handler := mockAppliance.checkAuthorization(nextHandler) + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != tt.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus) + } + + if tt.expectedStatus == http.StatusUnauthorized && nextHandlerCalled { + t.Error("Next handler was called after a 403 response") + } + }) + } } diff --git a/internal/appliance/config/BUILD.bazel b/internal/appliance/config/BUILD.bazel index b677e14acb6da..cecb1f94345a1 100644 --- a/internal/appliance/config/BUILD.bazel +++ b/internal/appliance/config/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "prometheus/default.yml.gotmpl", "postgres/codeinsights.conf", "grafana/default.yml.gotmpl", + "otel/agent.yaml", ], importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/config", tags = [TAG_INFRA_RELEASE], @@ -25,6 +26,7 @@ go_library( "//lib/pointers", "@io_k8s_api//core/v1:core", "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_sigs_controller_runtime//pkg/client", ], ) diff --git a/internal/appliance/config/annotations.go b/internal/appliance/config/annotations.go index 613bc7a0de5c0..388bac71ce3e6 100644 --- a/internal/appliance/config/annotations.go +++ b/internal/appliance/config/annotations.go @@ -1,7 +1,37 @@ package config +// Status is a point in the Appliance lifecycle that an Appliance can be in. +type Status string + +func (s Status) String() string { + return string(s) +} + const ( - AnnotationKeyManaged = "appliance.sourcegraph.com/managed" - AnnotationKeyCurrentVersion = "appliance.sourcegraph.com/currentVersion" - AnnotationKeyConfigHash = "appliance.sourcegraph.com/configHash" + ConfigmapName = "sourcegraph-appliance" + + AnnotationKeyManaged = "appliance.sourcegraph.com/managed" + AnnotationConditions = "appliance.sourcegraph.com/conditions" + AnnotationKeyCurrentVersion = "appliance.sourcegraph.com/currentVersion" + AnnotationKeyConfigHash = "appliance.sourcegraph.com/configHash" + AnnotationKeyShouldTakeOwnership = "appliance.sourcegraph.com/adopted" + + // TODO set status on configmap to communicate it across reboots + AnnotationKeyStatus = "appliance.sourcegraph.com/status" + + StatusUnknown Status = "unknown" + StatusInstall Status = "install" + StatusInstalling Status = "installing" + StatusUpgrading Status = "upgrading" + StatusWaitingForAdmin Status = "wait-for-admin" + StatusRefresh Status = "refresh" + StatusMaintenance Status = "maintenance" ) + +func IsPostInstallStatus(status Status) bool { + switch status { + case StatusUnknown, StatusInstall, StatusInstalling, StatusWaitingForAdmin: + return false + } + return true +} diff --git a/internal/appliance/config/config.go b/internal/appliance/config/config.go index 5fd350048ea5a..b4474f43a134b 100644 --- a/internal/appliance/config/config.go +++ b/internal/appliance/config/config.go @@ -1,6 +1,9 @@ package config -import corev1 "k8s.io/api/core/v1" +import ( + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) type StandardComponent interface { Disableable @@ -63,3 +66,20 @@ func (c StandardConfig) GetPrometheusPort() *int { return c.Prom func (c StandardConfig) GetServiceAccountAnnotations() map[string]string { return c.ServiceAccountAnnotations } + +func MarkObjectForAdoption(obj client.Object) { + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[AnnotationKeyShouldTakeOwnership] = "true" + obj.SetAnnotations(annotations) +} + +func ShouldAdopt(obj client.Object) bool { + if annotations := obj.GetAnnotations(); annotations != nil { + _, ok := annotations[AnnotationKeyShouldTakeOwnership] + return ok + } + return false +} diff --git a/internal/appliance/config/embed.go b/internal/appliance/config/embed.go index 1b008ede1f0fe..c591ac3b58dda 100644 --- a/internal/appliance/config/embed.go +++ b/internal/appliance/config/embed.go @@ -5,6 +5,7 @@ import ( ) var ( + //go:embed otel/* //go:embed postgres/* //go:embed prometheus/default.yml.gotmpl //go:embed grafana/default.yml.gotmpl @@ -15,6 +16,7 @@ var ( GrafanaDefaultConfigTemplate []byte CodeIntelConfig []byte CodeInsightsConfig []byte + OtelAgentConfig []byte ) func init() { @@ -23,6 +25,7 @@ func init() { PgsqlConfig = mustReadFile("postgres/pgsql.conf") PrometheusDefaultConfigTemplate = mustReadFile("prometheus/default.yml.gotmpl") GrafanaDefaultConfigTemplate = mustReadFile("grafana/default.yml.gotmpl") + OtelAgentConfig = mustReadFile("otel/agent.yaml") } func mustReadFile(name string) []byte { diff --git a/internal/appliance/config/otel/agent.yaml b/internal/appliance/config/otel/agent.yaml new file mode 100644 index 0000000000000..bc85c7aa0f8b6 --- /dev/null +++ b/internal/appliance/config/otel/agent.yaml @@ -0,0 +1,43 @@ +receivers: + otlp: + protocols: + grpc: # port 4317 + http: # port 4318 + +exporters: + otlp: + endpoint: "otel-collector:4317" + tls: + insecure: true + sending_queue: + num_consumers: 4 + queue_size: 100 + retry_on_failure: + enabled: true + +# TODO: allow configuring processors through values +#processors: +# batch: +# memory_limiter: +# # 80% of maximum memory up to 2G +# limit_mib: 400 +# # 25% of limit up to 2G +# spike_limit_mib: 100 +# check_interval: 5s + +extensions: + health_check: + endpoint: ":13133" + zpages: + endpoint: "localhost:55679" + +service: + extensions: + - zpages + - health_check + pipelines: + traces: + receivers: + - otlp + exporters: + - otlp diff --git a/internal/appliance/config/spec.go b/internal/appliance/config/spec.go index 439c999a2db6f..3bddbec8db757 100644 --- a/internal/appliance/config/spec.go +++ b/internal/appliance/config/spec.go @@ -88,6 +88,10 @@ type IndexedSearchSpec struct { Replicas int32 `json:"replicas,omitempty"` } +type OtelAgentSpec struct { + StandardConfig +} + type OtelCollectorSpec struct { StandardConfig } @@ -228,7 +232,8 @@ type SourcegraphSpec struct { Jaeger JaegerSpec `json:"jaeger,omitempty"` - OtelCollector OtelCollectorSpec `json:"openTelemetry,omitempty"` + OtelAgent OtelAgentSpec `json:"openTelemetryAgent,omitempty"` + OtelCollector OtelCollectorSpec `json:"openTelemetryCollector,omitempty"` // PGSQL defines the desired state of the PostgreSQL database. PGSQL PGSQLSpec `json:"pgsql,omitempty"` @@ -264,21 +269,33 @@ type SourcegraphSpec struct { StorageClass StorageClassSpec `json:"storageClass,omitempty"` } -// SetupStatus defines the observes status of the setup process. -type SetupStatus struct { - Progress int32 +// SourcegraphServicesToReconcile is a list of all Sourcegraph services that will be reconciled by appliance. +var SourcegraphServicesToReconcile = []string{ + "blobstore", + "repo-updater", + "symbols", + "gitserver", + "redis", + "pgsql", + "syntect", + "precise-code-intel", + "code-insights-db", + "code-intel-db", + "prometheus", + "cadvisor", + "worker", + "frontend", + "searcher", + "indexed-searcher", + "grafana", + "jaeger", + "otel", } // SourcegraphStatus defines the observed state of Sourcegraph type SourcegraphStatus struct { // CurrentVersion is the version of Sourcegraph currently running. CurrentVersion string `json:"currentVersion"` - - // Setup tracks the progress of the setup process. - Setup SetupStatus `json:"setup,omitempty"` - - // Represents the latest available observations of Sourcegraph's current state. - Conditions []metav1.Condition `json:"conditions,omitempty"` } // Sourcegraph is the Schema for the Sourcegraph API diff --git a/internal/appliance/embed.go b/internal/appliance/embed.go deleted file mode 100644 index 7178979f25073..0000000000000 --- a/internal/appliance/embed.go +++ /dev/null @@ -1,15 +0,0 @@ -package appliance - -import ( - "embed" - "io/fs" -) - -var ( - //go:embed web/static - staticFiles embed.FS - staticFS, _ = fs.Sub(staticFiles, "web/static") - - //go:embed web/template - templateFS embed.FS -) diff --git a/internal/appliance/errors.go b/internal/appliance/errors.go index 406eb8c38a252..d45a5c3020bb8 100644 --- a/internal/appliance/errors.go +++ b/internal/appliance/errors.go @@ -1,43 +1,41 @@ package appliance import ( + "fmt" "net/http" "github.com/sourcegraph/log" ) -const ( - queryKeyUserMessage = "sourcegraph-appliance-user-message" - errMsgSomethingWentWrong = "Something went wrong - please contact support." -) - -func (a *Appliance) redirectToErrorPage(w http.ResponseWriter, req *http.Request, userMsg string, err error, userError bool) { - a.redirectWithError(w, req, "/appliance/error", userMsg, err, userError) +func (a *Appliance) logError(r *http.Request, err error) { + a.logger.Error(err.Error(), log.String("method", r.Method), log.String("uri", r.URL.RequestURI())) } -func (a *Appliance) redirectWithError(w http.ResponseWriter, req *http.Request, path, userMsg string, err error, userError bool) { - logFn := a.logger.Error - if userError { - logFn = a.logger.Info +func (a *Appliance) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) { + resp := responseData{"error": message} + + if err := a.writeJSON(w, status, resp, nil); err != nil { + a.logError(r, err) } - logFn("an error occurred", log.Error(err)) - req = req.Clone(req.Context()) - req.URL.Path = path - queryValues := req.URL.Query() - queryValues.Set(queryKeyUserMessage, userMsg) - req.URL.RawQuery = queryValues.Encode() - http.Redirect(w, req, req.URL.String(), http.StatusFound) } -func (a *Appliance) errorHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if err := renderTemplate("error", w, struct { - Msg string - }{ - Msg: req.URL.Query().Get(queryKeyUserMessage), - }); err != nil { - a.handleError(w, err, "executing template") - return - } - }) +func (a *Appliance) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { + a.errorResponse(w, r, http.StatusBadRequest, err.Error()) +} + +func (a *Appliance) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { + a.logError(r, err) + a.errorResponse(w, r, http.StatusInternalServerError, "the server encountered a problem and could not process your request") +} + +func (a *Appliance) notFoundResponse(w http.ResponseWriter, r *http.Request) { + a.errorResponse(w, r, http.StatusNotFound, "the requested resource could not be found") +} + +func (a *Appliance) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) { + a.errorResponse(w, r, http.StatusMethodNotAllowed, fmt.Sprintf("the %s method is not supported", r.Method)) +} + +func (a *Appliance) invalidAdminPasswordResponse(w http.ResponseWriter, r *http.Request) { + a.errorResponse(w, r, http.StatusUnauthorized, "invalid admin password") } diff --git a/internal/appliance/frontend/maintenance/BUILD.bazel b/internal/appliance/frontend/maintenance/BUILD.bazel index 527ffe056396d..dad17c2d91a88 100644 --- a/internal/appliance/frontend/maintenance/BUILD.bazel +++ b/internal/appliance/frontend/maintenance/BUILD.bazel @@ -3,6 +3,9 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_config") load("@npm//:defs.bzl", "npm_link_all_packages") load("@npm//internal/appliance/frontend/maintenance:tsconfig-to-swcconfig/package_json.bzl", tsconfig_to_swcconfig = "bin") load("@npm//internal/appliance/frontend/maintenance:vite/package_json.bzl", vite_bin = "bin") +load("@rules_pkg//:pkg.bzl", "pkg_tar") +load("//wolfi-images:defs.bzl", "wolfi_base") +load("@container_structure_test//:defs.bzl", "container_structure_test") npm_link_all_packages( name = "node_modules", @@ -19,13 +22,12 @@ RUNTIME_DEPS = [ "src/Install.tsx", "src/Login.tsx", "src/Maintenance.tsx", - "src/OperatorDebugBar.tsx", "src/OperatorStatus.tsx", "src/Progress.tsx", "src/Theme.tsx", "src/WaitForAdmin.tsx", "src/api.ts", - "src/debugBar.ts", + "src/state.ts", "src/index.css", "src/main.tsx", "src/reportWebVitals.ts", @@ -104,6 +106,7 @@ js_run_binary( mnemonic = "ViteBuild", out_dirs = ["dist"], tool = ":vite", + visibility = ["//docker-images/appliance-frontend:__pkg__"], ) # Hosts the production-bundled application in a web server @@ -113,3 +116,38 @@ vite_bin.vite_binary( chdir = package_name(), data = [":build"], ) + +pkg_tar( + name = "tar_frontend", + srcs = [":build"], + package_dir = "maintenance", + strip_prefix = "dist", + visibility = ["//docker-images/appliance-frontend:__pkg__"], +) + +container_structure_test( + name = "image_test", + timeout = "short", + configs = ["image_test.yaml"], + driver = "docker", + image = "//docker-images/appliance-frontend:image", + tags = [ + "exclusive", + "requires-network", + TAG_INFRA_DEVINFRA, + ], +) + +filegroup( + name = "config", + srcs = ["maintenance.conf.template"], +) + +pkg_tar( + name = "tar_config", + srcs = [":config"], + package_dir = "/etc/nginx/templates", + visibility = ["//docker-images/appliance-frontend:__pkg__"], +) + +wolfi_base(target = "appliance-frontend") diff --git a/internal/appliance/frontend/maintenance/README.md b/internal/appliance/frontend/maintenance/README.md index 15ead240fd105..10590cbbda225 100644 --- a/internal/appliance/frontend/maintenance/README.md +++ b/internal/appliance/frontend/maintenance/README.md @@ -12,3 +12,17 @@ This will run the service locally, starting a Vite developer environment: pnpm install pnpm run dev + +## Wolfi image + +This will build and test the Wolfi image: + +### Building + + bazel build //docker-images/appliance-frontend:image + +### Testing + + bazel test \ + //internal/appliance/frontend/maintenance:image_test \ + //docker-images/appliance-frontend:image_test diff --git a/internal/appliance/frontend/maintenance/image_test.yaml b/internal/appliance/frontend/maintenance/image_test.yaml new file mode 100755 index 0000000000000..994c9f45b16ff --- /dev/null +++ b/internal/appliance/frontend/maintenance/image_test.yaml @@ -0,0 +1,12 @@ +schemaVersion: "2.0.0" + +commandTests: + - name: maintenance server available + command: /init.sh + args: + - stat + - /etc/nginx/conf.d/maintenance.conf + - name: maintenance app is available + command: stat + args: + - /maintenance/index.html diff --git a/internal/appliance/frontend/maintenance/maintenance.conf.template b/internal/appliance/frontend/maintenance/maintenance.conf.template new file mode 100755 index 0000000000000..b72ce320e6e81 --- /dev/null +++ b/internal/appliance/frontend/maintenance/maintenance.conf.template @@ -0,0 +1,49 @@ +# ____ ___ ___ _ _ ____ _ _ ____ ____ +# |__| |__] |__] | | |__| |\ | | |___ +# | | | | |___ | | | | \| |___ |___ +# +# _ _ ____ _ _ _ ___ ____ _ _ ____ _ _ ____ ____ +# |\/| |__| | |\ | | |___ |\ | |__| |\ | | |___ +# | | | | | | \| | |___ | \| | | | \| |___ |___ +# +# Sourcegraph Appliance Maintenance UI + +server { + listen 80; + listen [::]:80; + server_name localhost; + access_log off; + + + location / { + # Hideous char-mask to avoid nested ifs, which casue warnings in various + # config linters. nginx doesn't support boolean operators as far as I + # can tell. + set $redirect_mask 0; + if ($request_uri !~ ^/maintenance) { + set $redirect_mask 1; + } + if ($request_uri !~ ^/api) { + set $redirect_mask 1$redirect_mask; + } + if ($request_uri !~ ^/assets) { + set $redirect_mask 1$redirect_mask; + } + if ($redirect_mask = 111) { + return 302 $scheme://$host:$server_port/maintenance; + } + + root /maintenance; + index index.html index.htm; + } + + location /api/ { + proxy_pass ${API_ENDPOINT}/api/; + } + + error_page 404 /; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /maintenance; + } +} diff --git a/internal/appliance/frontend/maintenance/src/Frame.tsx b/internal/appliance/frontend/maintenance/src/Frame.tsx index b2267b0e05dc3..5f49e44601f64 100644 --- a/internal/appliance/frontend/maintenance/src/Frame.tsx +++ b/internal/appliance/frontend/maintenance/src/Frame.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { AppBar, Typography, useTheme } from '@mui/material' import { Outlet } from 'react-router-dom' @@ -7,12 +7,11 @@ import logo from '../assets/sourcegraph.png' import { adminPassword, call } from './api' import { Login } from './Login' -import { OperatorDebugBar } from './OperatorDebugBar' import { OperatorStatus } from './OperatorStatus' import { Info } from './Theme' -const FetchStateTimerMs = 1 * 1000 -const WaitToLoginAfterConnectMs = 1 * 1000 +const FetchStateTimerMs = 1000 +const WaitToLoginAfterConnectMs = 1000 export type stage = 'unknown' | 'install' | 'installing' | 'wait-for-admin' | 'upgrading' | 'maintenance' | 'refresh' @@ -29,7 +28,7 @@ export interface OutletContext { const fetchStatus = async (lastContext: OutletContext): Promise => new Promise(resolve => { - call('/api/operator/v1beta1/stage') + call('/api/v1/appliance/status') .then(result => { if (!result.ok) { if (result.status === 401) { @@ -49,7 +48,7 @@ const fetchStatus = async (lastContext: OutletContext): Promise = .then(result => { resolve({ online: true, - stage: result.stage, + stage: result.status.status, onlineDate: lastContext.onlineDate ?? Date.now(), }) }) @@ -102,9 +101,9 @@ export const Frame: React.FC = () => {
- + - Appliance + Sourcegraph Appliance
@@ -119,7 +118,6 @@ export const Frame: React.FC = () => { )}
-
) } diff --git a/internal/appliance/frontend/maintenance/src/Home.tsx b/internal/appliance/frontend/maintenance/src/Home.tsx index 63647ce91eb8f..50e090028406f 100644 --- a/internal/appliance/frontend/maintenance/src/Home.tsx +++ b/internal/appliance/frontend/maintenance/src/Home.tsx @@ -2,6 +2,8 @@ import { CircularProgress, Typography } from '@mui/material' import './App.css' +import React from 'react' + import { useOutletContext } from 'react-router-dom' import { OutletContext } from './Frame' diff --git a/internal/appliance/frontend/maintenance/src/Install.tsx b/internal/appliance/frontend/maintenance/src/Install.tsx index c4fa0e8648d61..3544a509d6293 100644 --- a/internal/appliance/frontend/maintenance/src/Install.tsx +++ b/internal/appliance/frontend/maintenance/src/Install.tsx @@ -1,86 +1,186 @@ -import { useState } from 'react' +import React, { useState, useEffect } from 'react' -import { Button, Checkbox, FormControl, InputLabel, MenuItem, Paper, Select, Stack, Typography } from '@mui/material' +import { + Button, + FormControl, + RadioGroup, + InputLabel, + MenuItem, + Paper, + Select, + Stack, + Typography, + Radio, + FormControlLabel, + FormGroup, + FormLabel, + FormHelperText, + Box, + TextField, + Tab, + Tabs, +} from '@mui/material' -import search from '../assets/sourcegraph.png' +import { changeStage } from './state' -import { changeStage } from './debugBar' +export const Install: React.FC = () => { + type installState = 'select-version' | 'select-db-type' + const [installState, setInstallState] = useState('select-version') -interface InstallerProps { - allowDisable: boolean -} + const [versions, setVersions] = useState([]) + const [selectedVersion, setSelectedVersion] = useState('') -export const Install: React.FC = () => { - const [version, setVersion] = useState('5.3.1') - const [installSearch, setInstallSearch] = useState(true) + type dbType = 'built-in' | 'external' + const [dbType, setDbType] = useState('built-in') - const install = () => { - changeStage({ action: 'installing', data: version }) + type dbTab = 'pgsql' | 'codeintel' | 'codeinsights' + const [dbTab, setDbTab] = useState('pgsql') + + const handleDbTabChange = (event: React.SyntheticEvent, newValue: dbTab) => { + setDbTab(newValue) } - const SearchInstaller: React.FC = ({ allowDisable = false }) => ( - setInstallSearch(prevSarch => !prevSarch) : undefined} - > - - - - Search Suite - - - Sourcegraph search suite: Code Search, Code Intelligence,
- Batch Changes, and Own. -
-
- -
- ) + useEffect(() => { + const fetchVersions = async () => { + try { + const response = await fetch('https://releaseregistry.sourcegraph.com/v1/releases/sourcegraph', { + headers: { + Authorization: `Bearer token`, + 'Content-Type': 'application/json', + }, + mode: 'cors', + }) + const data = await response.json() + setVersions(data) + if (data.length > 0) { + const publicVersions = data + .filter(item => item.public) + .filter(item => !item.is_development) + .map(item => item.version) + setVersions(publicVersions) + setSelectedVersion(publicVersions[0]) // Set the first version as default + } + } catch (error) { + console.error('Failed to fetch versions:', error) + + // Very basic fallback for when release registry is down: + // hardcode a particular version of Sourcegraph, which is the + // latest at the time of writing. + // This could be replaced with a fallback to a release registry + // response fixture that appliance-frontend has access to on the + // filesystem. In Kubernetes, this could be derived from a + // ConfigMap, with the files being distributed to airgap users + // out-of-band. + const publicVersions = ['v5.5.2463'] + setVersions(publicVersions) + setSelectedVersion(publicVersions[0]) + } + } + + fetchVersions() + }, []) - const allowInstall = installSearch + const next = () => { + if (selectedVersion === '') { + alert('Please select a version') + return + } + setInstallState('select-db-type') + } + + const back = () => { + setInstallState('select-version') + } + + const install = () => { + changeStage({ action: 'installing', data: selectedVersion }) + } + + const handleDbSelect = (event: React.ChangeEvent) => { + setDbType(event.target.value as dbType) + } return ( + // Render a version selection box followed by a database configuration screen, then an install prompt
- Install Sourcegraph Appliance + Setup Sourcegraph - - - Version - - - Select Components To Install -
- -
-
- {allowInstall ? ( - Press install to begin installation. - ) : ( - - Please select at least one component to install. - - )} -
- -
+ {installState === 'select-version' ? ( + + + Version + + +
+ Proceed to database configuration. +
+ +
+ ) : installState === 'select-db-type' ? ( + + + Configure Sourcegraph Databases + + + } label="built-in DBs" /> + + Selecting built-in dbs, configures sourcegraph to use built in databases. + Provisioned and controlled directly by appliance.{' '} + + } + label="External DBs (not yet supported)" + /> + + + + {dbType === 'external' ? ( + + + + + + + + + + + + + + + + + + + ) : null} + + + + + + ) : null}
) diff --git a/internal/appliance/frontend/maintenance/src/Login.tsx b/internal/appliance/frontend/maintenance/src/Login.tsx index 332b3deea24ed..095193ae9bad8 100644 --- a/internal/appliance/frontend/maintenance/src/Login.tsx +++ b/internal/appliance/frontend/maintenance/src/Login.tsx @@ -1,4 +1,4 @@ -import { createRef, useEffect, useState } from 'react' +import React, { createRef, useEffect, useState } from 'react' import Maintenance from '@mui/icons-material/Engineering' import { Box, Button, Paper, TextField, Typography } from '@mui/material' diff --git a/internal/appliance/frontend/maintenance/src/Maintenance.tsx b/internal/appliance/frontend/maintenance/src/Maintenance.tsx index 24b79ff8f24c5..b350e33249ac4 100644 --- a/internal/appliance/frontend/maintenance/src/Maintenance.tsx +++ b/internal/appliance/frontend/maintenance/src/Maintenance.tsx @@ -1,3 +1,4 @@ +import type React from 'react' import { Fragment, useEffect, useState } from 'react' import Unhealthy from '@mui/icons-material/CarCrashOutlined' @@ -6,10 +7,10 @@ import { Alert, Button, CircularProgress, Grid, Stack, Typography } from '@mui/m import classNames from 'classnames' import { call } from './api' -import { maintenance } from './debugBar' +import { maintenance } from './state' -const MaintenanceStatusTimerMs = 1 * 1000 -const WaitToLaunchFixMs = 5 * 1000 +const MaintenanceStatusTimerMs = 1000 +const WaitToLaunchFixMs = 5000 type Service = { name: string @@ -17,7 +18,7 @@ type Service = { message: string } -type Status = { +type ServiceStatuses = { services: Service[] } @@ -54,14 +55,14 @@ const ShowServices: React.FC<{ services: Service[] }> = ({ services }) => ) : null export const Maintenance: React.FC = () => { - const [status, setStatus] = useState() + const [serviceStatuses, setServiceStatuses] = useState() const [fixing, setFixing] = useState(false) useEffect(() => { const timer = setInterval(() => { - call('/api/operator/v1beta1/maintenance/status') + call('/api/v1/appliance/maintenance/serviceStatuses') .then(response => response.json()) - .then(setStatus) + .then(serviceStatuses => setServiceStatuses(serviceStatuses)) }, MaintenanceStatusTimerMs) return () => clearInterval(timer) }, []) @@ -75,8 +76,8 @@ export const Maintenance: React.FC = () => { } }, [fixing]) - const ready = status?.services.length !== undefined - const unhealthy = status?.services?.find((s: Service) => !s.healthy) + const ready = serviceStatuses?.services.length !== undefined + const unhealthy = serviceStatuses?.services?.find((s: Service) => !s.healthy) return (
@@ -97,7 +98,7 @@ export const Maintenance: React.FC = () => { {ready ? ( <> Service Status - + ) : null} diff --git a/internal/appliance/frontend/maintenance/src/OperatorDebugBar.tsx b/internal/appliance/frontend/maintenance/src/OperatorDebugBar.tsx deleted file mode 100644 index ee859efeefeac..0000000000000 --- a/internal/appliance/frontend/maintenance/src/OperatorDebugBar.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useEffect, useState } from 'react' - -import { Button, Paper, Stack, Typography } from '@mui/material' - -import { call } from './api' -import { changeStage, maintenance } from './debugBar' -import { ContextProps, stage } from './Frame' - -const DebugBarTimerMs = 1 * 1000 - -export const OperatorDebugBar: React.FC = ({ context }) => { - const [waiting, setWaiting] = useState(false) - - const setStage = (action: stage, data?: string) => changeStage({ action, data, onDone: () => setWaiting(true) }) - - const startInstall = () => setStage('install') - const installProgress = () => setStage('installing') - const installWaitAdmin = () => setStage('wait-for-admin') - const upgradeProgress = () => setStage('upgrading', '5.4.0 (beta1)') - const noState = () => setStage('unknown') - const launchAdminUI = () => setStage('refresh') - const failInstall = () => { - call('/api/operator/v1beta1/fake/install/fail', { - method: 'POST', - }).then(() => { - setWaiting(true) - }) - } - const setMaintenance = ({ healthy }: { healthy: boolean }) => - maintenance({ healthy, onDone: () => setWaiting(true) }) - - useEffect(() => { - const timer = setInterval(() => { - if (waiting) { - setWaiting(false) - } - }, DebugBarTimerMs) - return () => clearInterval(timer) - }, [waiting]) - - const showDebugBar = localStorage.getItem('debugbar') === 'true' - - return ( - context.online && - showDebugBar && ( - - - Operator Debug Controls - - - Installation - - - - - - - - - - - - - Maintenance - - - - - Reset - - - - Upgrade - - - - - - - ) - ) -} diff --git a/internal/appliance/frontend/maintenance/src/OperatorStatus.tsx b/internal/appliance/frontend/maintenance/src/OperatorStatus.tsx index f9751ae36636a..861ae11191432 100644 --- a/internal/appliance/frontend/maintenance/src/OperatorStatus.tsx +++ b/internal/appliance/frontend/maintenance/src/OperatorStatus.tsx @@ -1,3 +1,5 @@ +import React from 'react' + import styledReact from '@emotion/styled' import { styled } from '@mui/material' import { Navigate } from 'react-router-dom' @@ -20,7 +22,7 @@ export const OperatorStatus: React.FC = ({ context }) => { const Status = () => context.online === undefined ? (
connecting
- ) : context.online === true || context.needsLogin === true ? ( + ) : context.online || context.needsLogin ? (
@@ -32,14 +34,14 @@ export const OperatorStatus: React.FC = ({ context }) => { switch (context.stage) { case 'refresh': - document.location = '/?cacheBust=' + Date.now() + document.location.reload() break } return (
Status: - {context.online === false && } + {!context.online && } {context.stage === 'unknown' && } {context.stage === 'install' && } {context.stage === 'installing' && } diff --git a/internal/appliance/frontend/maintenance/src/Progress.tsx b/internal/appliance/frontend/maintenance/src/Progress.tsx index c0ed41e89c67a..ccfe64744899a 100644 --- a/internal/appliance/frontend/maintenance/src/Progress.tsx +++ b/internal/appliance/frontend/maintenance/src/Progress.tsx @@ -92,13 +92,13 @@ export const Progress: React.FC<{ useEffect(() => { const timer = setInterval(() => { - call('/api/operator/v1beta1/install/progress') + call('/api/v1/appliance/install/progress') .then(result => result.json()) .then(result => { - setVersion(result.version) - setProgress(result.progress) - setError(result.error) - setTasks(result.tasks) + setVersion(result.progress.version) + setProgress(result.progress.progress) + setError(result.progress.error) + setTasks(result.progress.tasks) }) .catch(err => setError(err.message)) }, 1000) diff --git a/internal/appliance/frontend/maintenance/src/Theme.tsx b/internal/appliance/frontend/maintenance/src/Theme.tsx index bf5bb489b2bba..a2365749d1858 100644 --- a/internal/appliance/frontend/maintenance/src/Theme.tsx +++ b/internal/appliance/frontend/maintenance/src/Theme.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react' +import React, { PropsWithChildren, createContext, useContext, useMemo, useState } from 'react' import { DarkModeOutlined, LightModeOutlined } from '@mui/icons-material' import { CssBaseline, ThemeProvider as MuiThemeProvider, PaletteMode, Theme, createTheme } from '@mui/material' @@ -15,7 +15,7 @@ export const Context = createContext({ theme: createTheme(), }) -export const ThemeProvider: React.FC = ({ children }) => { +export const ThemeProvider: React.FC> = ({ children }) => { const [mode, setMode] = useState((localStorage.getItem('theme') as PaletteMode) ?? 'light') const theme = useMemo(() => { diff --git a/internal/appliance/frontend/maintenance/src/WaitForAdmin.tsx b/internal/appliance/frontend/maintenance/src/WaitForAdmin.tsx index 5048c418a796c..4b4cf14a468f2 100644 --- a/internal/appliance/frontend/maintenance/src/WaitForAdmin.tsx +++ b/internal/appliance/frontend/maintenance/src/WaitForAdmin.tsx @@ -1,14 +1,12 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { Button, CircularProgress, Stack, Typography } from '@mui/material' -import { changeStage } from './debugBar' +import { changeStage } from './state.ts' -const TestAdminUIGoodMs = 1 * 1000 const WaitBeforeLaunchMs = 3 * 1000 export const WaitForAdmin: React.FC = () => { - const [waitingForBalancer, setWaitingForBalancer] = useState(false) const [launching, setLaunching] = useState(false) useEffect(() => { @@ -20,24 +18,9 @@ export const WaitForAdmin: React.FC = () => { } }, [launching]) - useEffect(() => { - const timer = setInterval(() => { - fetch('/sign-in') - .then(result => { - console.log('waiting for admin ui', result) - if (result.ok) { - setLaunching(true) - setWaitingForBalancer(false) - } - }) - .catch(console.error) - }, TestAdminUIGoodMs) - return () => clearInterval(timer) - }, [waitingForBalancer]) - return (
- Waiting For The Admin To Return + Waiting For The Admin To Return
The appliance is ready. We were waiting for you to set its security before opening it up. @@ -46,11 +29,7 @@ export const WaitForAdmin: React.FC = () => { Now that you're back, please press the button below to launch the Administration UI.
- {launching && ( @@ -59,12 +38,6 @@ export const WaitForAdmin: React.FC = () => { Launching Admin UI... Please wait... )} - {waitingForBalancer && ( - - - Waiting for Admin UI to be ready... Please wait... - - )}
) } diff --git a/internal/appliance/frontend/maintenance/src/debugBar.ts b/internal/appliance/frontend/maintenance/src/debugBar.ts deleted file mode 100644 index 48802ef081f16..0000000000000 --- a/internal/appliance/frontend/maintenance/src/debugBar.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { call } from './api' -import { stage } from './Frame' - -export const maintenance = ({ healthy, onDone }: { healthy: boolean; onDone?: () => void }): Promise => { - return call('/api/operator/v1beta1/fake/maintenance/healthy', { - method: 'POST', - body: JSON.stringify({ healthy: healthy }), - }) - .then(() => { - call('/api/operator/v1beta1/fake/stage', { - method: 'POST', - body: JSON.stringify({ stage: 'maintenance' }), - }).then(() => { - if (onDone !== undefined) { - onDone() - } - }) - }) - .then(() => { - if (onDone !== undefined) { - onDone() - } - }) -} - -export const changeStage = ({ action, data, onDone }: { action: stage; data?: string; onDone?: () => void }) => { - call('/api/operator/v1beta1/fake/stage', { - method: 'POST', - body: JSON.stringify({ stage: action, data }), - }).then(() => { - if (onDone) { - onDone() - } - }) -} diff --git a/internal/appliance/frontend/maintenance/src/reportWebVitals.ts b/internal/appliance/frontend/maintenance/src/reportWebVitals.ts index 959efc275bcd4..82aae6fdb1b84 100644 --- a/internal/appliance/frontend/maintenance/src/reportWebVitals.ts +++ b/internal/appliance/frontend/maintenance/src/reportWebVitals.ts @@ -1,5 +1,7 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { +import { ReportHandler } from 'web-vitals' + +const reportWebVitals = (onPerfEntry: ReportHandler) => { + if (onPerfEntry) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry) getFID(onPerfEntry) diff --git a/internal/appliance/frontend/maintenance/src/state.ts b/internal/appliance/frontend/maintenance/src/state.ts new file mode 100644 index 0000000000000..d5eaf0bd3e26b --- /dev/null +++ b/internal/appliance/frontend/maintenance/src/state.ts @@ -0,0 +1,31 @@ +import { call } from './api' +import { stage } from './Frame' + +export const maintenance = async ({ healthy, onDone }: { healthy: boolean; onDone?: () => void }): Promise => { + await call('/api/operator/v1beta1/fake/maintenance/healthy', { + method: 'POST', + body: JSON.stringify({ healthy: healthy }), + }) + call('/v1/appliance/status', { + method: 'POST', + body: JSON.stringify({ stage: 'maintenance' }), + }).then(() => { + if (onDone !== undefined) { + onDone() + } + }) + if (onDone !== undefined) { + onDone() + } +} + +export const changeStage = ({ action, data, onDone }: { action: stage; data?: string; onDone?: () => void }) => { + call('/api/v1/appliance/status', { + method: 'POST', + body: JSON.stringify({ state: action, data }), + }).then(() => { + if (onDone) { + onDone() + } + }) +} diff --git a/internal/appliance/healthchecker/BUILD.bazel b/internal/appliance/healthchecker/BUILD.bazel new file mode 100644 index 0000000000000..9c7ce10b2a578 --- /dev/null +++ b/internal/appliance/healthchecker/BUILD.bazel @@ -0,0 +1,44 @@ +load("//dev:go_defs.bzl", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "healthchecker", + srcs = [ + "health_checker.go", + "probe.go", + ], + importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/healthchecker", + visibility = ["//:__subpackages__"], + deps = [ + "//lib/errors", + "@com_github_sourcegraph_log//:log", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/labels", + "@io_k8s_sigs_controller_runtime//pkg/client", + ], +) + +go_test( + name = "healthchecker_test", + srcs = ["health_checker_test.go"], + data = [ + "//dev/tools:kubebuilder-assets", + ], + embed = [":healthchecker"], + env = { + "KUBEBUILDER_ASSET_PATHS": "$(rlocationpaths //dev/tools:kubebuilder-assets)", + }, + deps = [ + "//internal/appliance/k8senvtest", + "//internal/k8s/resource/service", + "@com_github_sourcegraph_log//:log", + "@com_github_sourcegraph_log//logtest", + "@com_github_sourcegraph_log_logr//:logr", + "@com_github_stretchr_testify//require", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_apimachinery//pkg/types", + "@io_k8s_apimachinery//pkg/util/intstr", + "@io_k8s_sigs_controller_runtime//pkg/client", + ], +) diff --git a/internal/appliance/healthchecker/health_checker.go b/internal/appliance/healthchecker/health_checker.go new file mode 100644 index 0000000000000..398ab0a79ad6d --- /dev/null +++ b/internal/appliance/healthchecker/health_checker.go @@ -0,0 +1,95 @@ +package healthchecker + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type Probe interface { + CheckPods(ctx context.Context, labelSelector, namespace string) error +} + +type HealthChecker struct { + Probe Probe + K8sClient client.Client + Logger log.Logger + + ServiceName client.ObjectKey + Interval time.Duration + Graceperiod time.Duration +} + +// ManageIngressFacingService waits for the `begin` channel to close, then periodically monitors the frontend +// service (the ingress-facing service). When there is at least one ready +// frontend pod, it ensures that the service points at the frontend pods. When +// there are no ready pods, it ensures that the service points to the appliance, +// so that the admin can log in and view maintenance status. +func (h *HealthChecker) ManageIngressFacingService(ctx context.Context, begin <-chan struct{}, labelSelector, namespace string) error { + h.Logger.Info("waiting for signal to begin managing ingress-facing service for the appliance") + select { + case <-begin: + // block + + case <-ctx.Done(): + h.Logger.Error("context done, exiting", log.Error(ctx.Err())) + return ctx.Err() + } + + h.Logger.Info("will periodically check health of frontend and re-point ingress appropriately") + + ticker := time.NewTicker(h.Interval) + defer ticker.Stop() + + // Do one iteration without having to wait for the first tick + if err := h.maybeFlipServiceOnce(ctx, labelSelector, namespace); err != nil { + return err + } + for { + select { + case <-ticker.C: + if err := h.maybeFlipServiceOnce(ctx, labelSelector, namespace); err != nil { + return err + } + + case <-ctx.Done(): + h.Logger.Error("context done, exiting", log.Error(ctx.Err())) + return ctx.Err() + } + } +} + +func (h *HealthChecker) maybeFlipServiceOnce(ctx context.Context, labelSelector, namespace string) error { + h.Logger.Info("checking deployment health") + if err := h.Probe.CheckPods(ctx, labelSelector, namespace); err != nil { + h.Logger.Error("found unhealthy state, waiting for the grace period", log.Error(err), log.String("gracePeriod", h.Graceperiod.String())) + time.Sleep(h.Graceperiod) + if err := h.Probe.CheckPods(ctx, labelSelector, namespace); err != nil { + h.Logger.Error("found unhealthy state, setting service selector to appliance", log.Error(err)) + return h.setServiceSelector(ctx, "sourcegraph-appliance-frontend") + } + } + + h.Logger.Info("deployment healthy") + return h.setServiceSelector(ctx, "sourcegraph-frontend") +} + +func (h *HealthChecker) setServiceSelector(ctx context.Context, to string) error { + h.Logger.Info("setting service selector", log.String("to", to)) + + var svc corev1.Service + if err := h.K8sClient.Get(ctx, h.ServiceName, &svc); err != nil { + h.Logger.Error("getting service", log.Error(err)) + return errors.Wrap(err, "getting service") + } + + // no-op if the selector is unchanged + svc.Spec.Selector["app"] = to + return h.K8sClient.Update(ctx, &svc) +} diff --git a/internal/appliance/healthchecker/health_checker_test.go b/internal/appliance/healthchecker/health_checker_test.go new file mode 100644 index 0000000000000..7032883aa77b6 --- /dev/null +++ b/internal/appliance/healthchecker/health_checker_test.go @@ -0,0 +1,164 @@ +package healthchecker + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/log" + "github.com/sourcegraph/log/logr" + "github.com/sourcegraph/log/logtest" + "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/service" +) + +var ( + // set once, before suite runs. See TestMain + ctx context.Context + k8sClient client.Client +) + +func TestMain(m *testing.M) { + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + + logger := log.Scoped("appliance-healthchecker-tests") + k8sConfig, cleanup, err := k8senvtest.SetupEnvtest(ctx, logr.New(logger), k8senvtest.NewNoopReconciler) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer func() { + if err := cleanup(); err != nil { + fmt.Println(err) + os.Exit(1) + } + }() + + k8sClient, err = client.New(k8sConfig, client.Options{}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + rc := m.Run() + + // Our earlier defer won't run after we call os.Exit() below + if err := cleanup(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + os.Exit(rc) +} + +// A bit of a lengthy scenario-style test +func TestManageIngressFacingService(t *testing.T) { + ns, err := k8senvtest.NewRandomNamespace("test-appliance-self-update") + require.NoError(t, err) + err = k8sClient.Create(ctx, ns) + require.NoError(t, err) + + serviceName := types.NamespacedName{Namespace: ns.GetName(), Name: "sourcegraph-frontend"} + checker := &HealthChecker{ + Probe: &PodProbe{K8sClient: k8sClient}, + K8sClient: k8sClient, + Logger: logtest.Scoped(t), + + ServiceName: serviceName, + Graceperiod: 0, + } + + // Simulate helm having created the service, but no frontend pods have been + // created yet + svc := service.NewService("sourcegraph-frontend", ns.GetName(), nil) + svc.Spec.Ports = []corev1.ServicePort{ + {Name: "http", Port: 30080, TargetPort: intstr.FromString("http")}, + } + svc.Spec.Selector = map[string]string{ + "app": "sourcegraph-appliance-frontend", + } + err = k8sClient.Create(ctx, &svc) + require.NoError(t, err) + runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend") + + // Simulate some frontend pods existing but with no readiness conditions. + pod1 := mkPod("pod1", ns.GetName()) + err = k8sClient.Create(ctx, pod1) + require.NoError(t, err) + pod2 := mkPod("pod2", ns.GetName()) + err = k8sClient.Create(ctx, pod2) + require.NoError(t, err) + runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend") + + // Simulate one pod becoming ready to receive traffic + pod1.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + } + err = k8sClient.Status().Update(ctx, pod1) + require.NoError(t, err) + pod2.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + }, + } + err = k8sClient.Status().Update(ctx, pod2) + require.NoError(t, err) + runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-frontend") + + // test idempotency of the monitor + runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-frontend") + + // Simulate pods becoming unready + pod1.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + }, + } + err = k8sClient.Status().Update(ctx, pod1) + require.NoError(t, err) + runHealthCheckAndAssertSelector(t, checker, serviceName, ns.GetName(), "sourcegraph-appliance-frontend") +} + +func runHealthCheckAndAssertSelector(t *testing.T, checker *HealthChecker, serviceName types.NamespacedName, namespace, expectedSelectorValue string) { + err := checker.maybeFlipServiceOnce(ctx, "app=sourcegraph-frontend", namespace) + require.NoError(t, err) + + var svc corev1.Service + err = k8sClient.Get(ctx, serviceName, &svc) + require.NoError(t, err) + + require.Equal(t, expectedSelectorValue, svc.Spec.Selector["app"]) +} + +func mkPod(name, namespace string) *corev1.Pod { + ctr := corev1.Container{ + Name: "frontend", + Image: "foo:bar", + Command: []string{"doitnow"}, + } + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app": "sourcegraph-frontend"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ctr}, + }, + } +} diff --git a/internal/appliance/healthchecker/probe.go b/internal/appliance/healthchecker/probe.go new file mode 100644 index 0000000000000..140e8cfb2157c --- /dev/null +++ b/internal/appliance/healthchecker/probe.go @@ -0,0 +1,38 @@ +package healthchecker + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type PodProbe struct { + K8sClient client.Client +} + +func (p *PodProbe) CheckPods(ctx context.Context, labelSelector, namespace string) error { + var pods corev1.PodList + selector, err := labels.Parse(labelSelector) + if err != nil { + return errors.Wrap(err, "parsing label selector") + } + if err := p.K8sClient.List(ctx, &pods, &client.ListOptions{LabelSelector: selector, Namespace: namespace}); err != nil { + return errors.Wrap(err, "listing pods") + } + for _, pod := range pods.Items { + for _, condition := range pod.Status.Conditions { + if condition.Type == corev1.PodReady { + if condition.Status == corev1.ConditionTrue { + // Return no error if even a single pod is ready + return nil + } + } + } + } + + return errors.New("no pods are ready") +} diff --git a/internal/appliance/html.go b/internal/appliance/html.go index 669f38de1c7a6..74f329c100845 100644 --- a/internal/appliance/html.go +++ b/internal/appliance/html.go @@ -2,150 +2,12 @@ package appliance import ( "context" - "fmt" - "html/template" - "io" - "net/http" - "path/filepath" - "time" - "github.com/golang-jwt/jwt/v5" "github.com/life4/genesis/slices" - passwordvalidator "github.com/wagslane/go-password-validator" - "golang.org/x/crypto/bcrypt" - "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/internal/appliance/config" "github.com/sourcegraph/sourcegraph/internal/releaseregistry" - "github.com/sourcegraph/sourcegraph/lib/errors" ) -const ( - formValueOn = "on" -) - -func templatePath(name string) string { - return filepath.Join("web", "template", name+".gohtml") -} - -func (a *Appliance) applianceHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ok, _ := a.shouldSetupRun(context.Background()); ok { - http.Redirect(w, r, "/appliance/setup", http.StatusSeeOther) - } - }) -} - -func renderTemplate(name string, w io.Writer, data any) error { - tmpl, err := template.ParseFS(templateFS, templatePath("layout"), templatePath(name)) - if err != nil { - return errors.Wrapf(err, "rendering template: %s", name) - } - return tmpl.Execute(w, data) -} - -func (a *Appliance) getSetupHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - versions, err := a.getVersions(r.Context()) - if err != nil { - a.handleError(w, err, "getting versions") - return - } - versions, err = NMinorVersions(versions, a.latestSupportedVersion, 2) - if err != nil { - a.handleError(w, err, "filtering versions to 2 minor points") - return - } - - err = renderTemplate("setup", w, struct { - Versions []string - }{ - Versions: versions, - }) - if err != nil { - a.handleError(w, err, "executing template") - return - } - }) -} - -func (a *Appliance) getLoginHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if len(a.adminPasswordBcrypt) == 0 { - msg := fmt.Sprintf( - "You must set a password: please create a secret named '%s' with key '%s'.", - initialPasswordSecretName, - initialPasswordSecretPasswordKey, - ) - a.redirectToErrorPage(w, r, msg, errors.New("no admin password set"), true) - return - } - - if err := renderTemplate("landing", w, struct { - Flash string - }{ - Flash: r.URL.Query().Get(queryKeyUserMessage), - }); err != nil { - a.handleError(w, err, "executing template") - return - } - }) -} - -func (a *Appliance) postLoginHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userSuppliedPassword := r.FormValue("password") - if err := bcrypt.CompareHashAndPassword(a.adminPasswordBcrypt, []byte(userSuppliedPassword)); err != nil { - if err == bcrypt.ErrMismatchedHashAndPassword { - a.redirectWithError(w, r, r.URL.Path, "Supplied password is incorrect.", err, true) - return - } - - a.redirectToErrorPage(w, r, errMsgSomethingWentWrong, err, false) - return - } - - if err := passwordvalidator.Validate(userSuppliedPassword, 60); err != nil { - msg := fmt.Sprintf( - "Please set a stronger password: delete the '%s' secret, and create a new secret named '%s' with key '%s'.", - dataSecretName, - initialPasswordSecretName, - initialPasswordSecretPasswordKey, - ) - a.redirectToErrorPage(w, r, msg, err, true) - return - } - - validUntil := time.Now().Add(time.Hour).UTC() - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - jwtClaimsValidUntilKey: validUntil.Format(time.RFC3339), - }) - tokenStr, err := token.SignedString(a.jwtSecret) - if err != nil { - a.handleError(w, err, errMsgSomethingWentWrong) - return - } - - http.SetCookie(w, &http.Cookie{ - Name: authCookieName, - Value: tokenStr, - Expires: validUntil, - }) - http.Redirect(w, r, "/appliance", http.StatusFound) - }) -} - -func (a *Appliance) handleError(w http.ResponseWriter, err error, msg string) { - a.logger.Error(msg, log.Error(err)) - - // TODO we should probably look twice at this and decide whether it's in - // line with existing standards. - // Don't leak details of internal errors to users - that's why we have - // logging above. - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintln(w, errMsgSomethingWentWrong) -} - func (a *Appliance) getVersions(ctx context.Context) ([]string, error) { versions, err := a.releaseRegistryClient.ListVersions(ctx, "sourcegraph") if err != nil { @@ -155,52 +17,3 @@ func (a *Appliance) getVersions(ctx context.Context) ([]string, error) { return version.Version, version.Public }), nil } - -func (a *Appliance) postSetupHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - a.logger.Error("failed to parse http form request", log.Error(err)) - // Handle err - } - - a.sourcegraph.Spec.RequestedVersion = r.FormValue("version") - if r.FormValue("external_database") == formValueOn { - a.sourcegraph.Spec.PGSQL.DatabaseConnection = &config.DatabaseConnectionSpec{ - Host: r.FormValue("pgsqlDBHost"), - Port: r.FormValue("pgsqlDBPort"), - User: r.FormValue("pgsqlDBUser"), - Password: r.FormValue("pgsqlDBPassword"), - Database: r.FormValue("pgsqlDBName"), - } - a.sourcegraph.Spec.CodeIntel.DatabaseConnection = &config.DatabaseConnectionSpec{ - Host: r.FormValue("codeintelDBHost"), - Port: r.FormValue("codeintelDBPort"), - User: r.FormValue("codeintelDBUser"), - Password: r.FormValue("codeintelDBPassword"), - Database: r.FormValue("codeintelDBName"), - } - a.sourcegraph.Spec.CodeInsights.DatabaseConnection = &config.DatabaseConnectionSpec{ - Host: r.FormValue("codeinsightsDBHost"), - Port: r.FormValue("codeinsightsDBPort"), - User: r.FormValue("codeinsightsDBUser"), - Password: r.FormValue("codeinsightsDBPassword"), - Database: r.FormValue("codeinsightsDBName"), - } - } - // TODO validate user input - - if r.FormValue("dev_mode") == formValueOn { - a.sourcegraph.SetLocalDevMode() - } - - _, err = a.CreateConfigMap(r.Context(), "sourcegraph-appliance") - if err != nil { - a.logger.Error("failed to create configMap sourcegraph-appliance", log.Error(err)) - // Handle err - } - a.status = StatusInstalling - - http.Redirect(w, r, "/appliance", http.StatusSeeOther) - }) -} diff --git a/internal/appliance/json.go b/internal/appliance/json.go new file mode 100644 index 0000000000000..3c734f10c4984 --- /dev/null +++ b/internal/appliance/json.go @@ -0,0 +1,213 @@ +package appliance + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph/internal/appliance/config" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const maxBytes = 1_048_576 + +type responseData map[string]any + +func (a *Appliance) writeJSON(w http.ResponseWriter, status int, data responseData, headers http.Header) error { + js, err := json.MarshalIndent(data, "", "\t") + if err != nil { + return err + } + + js = append(js, '\n') + + for key, value := range headers { + w.Header()[key] = value + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, err = w.Write(js) + if err != nil { + return err + } + + return nil +} + +func (a *Appliance) readJSON(w http.ResponseWriter, r *http.Request, output any) error { + r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) + + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(output) + if err != nil { + var jsonMaxBytesErrorType *http.MaxBytesError + var jsonSyntaxErrorType *json.SyntaxError + var jsonUnmarshalErrorType *json.UnmarshalTypeError + var jsonInvalidUnmarshalErrorType *json.InvalidUnmarshalError + + // list of de-facto errors common to JSON APIs that we want to wrap and handle + switch { + case strings.HasPrefix(err.Error(), "json: unknown field"): + return errors.Newf("request body contains unknown key") + + case errors.Is(err, io.EOF): + return errors.New("request body must not be empty") + + case errors.Is(err, io.ErrUnexpectedEOF): + return errors.New("malformed JSON contained in request body") + + case errors.As(err, &jsonSyntaxErrorType): + return errors.Newf("malformed JSON found at character %d", jsonSyntaxErrorType.Offset) + + case errors.As(err, &jsonMaxBytesErrorType): + return errors.Newf("request body larger than %d bytes", jsonMaxBytesErrorType.Limit) + + case errors.As(err, &jsonUnmarshalErrorType): + if jsonUnmarshalErrorType.Field != "" { + return errors.Newf("incorrect JSON type for field %q", jsonUnmarshalErrorType.Field) + } + return errors.Newf("incorrect JSON type found at character %d", jsonUnmarshalErrorType.Offset) + + case errors.As(err, &jsonInvalidUnmarshalErrorType): + panic(err) + + default: + return err + } + } + + err = decoder.Decode(&struct{}{}) + if !errors.Is(err, io.EOF) { + return errors.New("request body must only contain single JSON value") + } + + return nil +} + +func (a *Appliance) getStatusJSONHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data := struct { + Status string `json:"status"` + Data string `json:"data,omitempty"` + }{ + Status: a.status.String(), + Data: "", + } + + if err := a.writeJSON(w, http.StatusOK, responseData{"status": data}, http.Header{}); err != nil { + a.serverErrorResponse(w, r, err) + } + }) +} + +func (a *Appliance) getInstallProgressJSONHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + currentTasks, progress := calculateProgress(installTasks()) + + installProgress := struct { + Version string `json:"version"` + Progress int `json:"progress"` + Error string `json:"error"` + Tasks []Task `json:"tasks"` + }{ + Version: "", + Progress: progress, + Error: "", + Tasks: currentTasks, + } + + ok, err := a.isSourcegraphFrontendReady(r.Context()) + if err != nil { + a.logger.Error("failed to get sourcegraph frontend status") + return + } + + if ok { + a.status = config.StatusWaitingForAdmin + } + + if err := a.writeJSON(w, http.StatusOK, responseData{"progress": installProgress}, http.Header{}); err != nil { + a.serverErrorResponse(w, r, err) + } + }) +} + +func (a *Appliance) getMaintenanceStatusHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + type service struct { + Name string `json:"name"` + Healthy bool `json:"healthy"` + Message string `json:"message"` + } + + services := []service{} + for _, name := range config.SourcegraphServicesToReconcile { + services = append(services, service{ + Name: name, + Healthy: true, + Message: "fake event", + }) + } + fmt.Println(services) + if err := a.writeJSON(w, http.StatusOK, responseData{"services": services}, http.Header{}); err != nil { + a.serverErrorResponse(w, r, err) + } + }) +} + +func (a *Appliance) postStatusJSONHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var input struct { + State string `json:"state"` + Data string `json:"data,omitempty"` + } + + if err := a.readJSON(w, r, &input); err != nil { + a.badRequestResponse(w, r, err) + return + } + + newStatus := config.Status(input.State) + a.logger.Info("state transition", log.String("state", string(newStatus))) + // trim v if v exists + input.Data = strings.TrimPrefix(input.Data, "v") + a.sourcegraph.Spec.RequestedVersion = input.Data + if err := a.setStatus(r.Context(), newStatus); err != nil { + if kerrors.IsNotFound(err) { + a.logger.Info("no configmap found, will not set status") + } else { + a.serverErrorResponse(w, r, err) + return + } + } + + if a.noResourceRestrictions { + a.sourcegraph.SetLocalDevMode() + } + + cfgMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sourcegraph-appliance", + Namespace: a.namespace, + }, + } + err := a.reconcileConfigMap(r.Context(), cfgMap) + if err != nil { + a.serverErrorResponse(w, r, err) + } + + a.status = newStatus + }) +} diff --git a/internal/appliance/json_test.go b/internal/appliance/json_test.go new file mode 100644 index 0000000000000..c2e65a3d269c5 --- /dev/null +++ b/internal/appliance/json_test.go @@ -0,0 +1,230 @@ +package appliance + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sourcegraph/log" +) + +func TestReadJSON(t *testing.T) { + appliance := &Appliance{ + logger: log.NoOp(), + } + + t.Run("ValidJSON", func(t *testing.T) { + body := `{"key": "value"}` + req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + var output map[string]string + err := appliance.readJSON(w, req, &output) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if diff := cmp.Diff(map[string]string{"key": "value"}, output); diff != "" { + t.Errorf("output mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("EmptyBody", func(t *testing.T) { + req := httptest.NewRequest("POST", "/", nil) + w := httptest.NewRecorder() + + var output map[string]string + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if err.Error() != "request body must not be empty" { + t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body must not be empty") + } + }) + + t.Run("MalformedJSON", func(t *testing.T) { + body := `{"key": "value",}` + req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + var output map[string]string + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if !strings.HasPrefix(err.Error(), "malformed JSON found at character") { + t.Errorf("unexpected error message: %v", err) + } + }) + + t.Run("UnknownField", func(t *testing.T) { + body := `{"unknown_field": "value"}` + req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + var output struct{} + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if err.Error() != "request body contains unknown key" { + t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body contains unknown key") + } + }) + + t.Run("IncorrectJSONType", func(t *testing.T) { + body := `{"key": 123}` + req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + var output struct { + Key string `json:"key"` + } + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if err.Error() != `incorrect JSON type for field "key"` { + t.Errorf("unexpected error message: got %q, want %q", err.Error(), `incorrect JSON type for field "key"`) + } + }) + + t.Run("MultipleJSONValues", func(t *testing.T) { + body := `{"key1": "value1"}{"key2": "value2"}` + req := httptest.NewRequest("POST", "/", strings.NewReader(body)) + w := httptest.NewRecorder() + + var output map[string]string + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if err.Error() != "request body must only contain single JSON value" { + t.Errorf("unexpected error message: got %q, want %q", err.Error(), "request body must only contain single JSON value") + } + }) + + t.Run("LargeBody", func(t *testing.T) { + // Create a large JSON object + largeObject := map[string]string{} + for i := 0; i < maxBytes/10; i++ { + key := fmt.Sprintf("key%d", i) + largeObject[key] = strings.Repeat("a", 10) + } + + largeJSON, _ := json.Marshal(largeObject) + // Ensure the JSON is larger than maxBytes + largeJSON = append(largeJSON, []byte(`,"extra":"data"}`)...) + + req := httptest.NewRequest("POST", "/", bytes.NewReader(largeJSON)) + w := httptest.NewRecorder() + + var output map[string]string + err := appliance.readJSON(w, req, &output) + + if err == nil { + t.Error("expected an error, got nil") + } else if !strings.HasPrefix(err.Error(), "request body larger than") { + t.Errorf("unexpected error message: %v", err) + } + }) +} + +func TestWriteJSON(t *testing.T) { + appliance := &Appliance{ + logger: log.NoOp(), + } + + tests := []struct { + name string + status int + data responseData + headers http.Header + expected string + }{ + { + name: "Simple JSON response", + status: http.StatusOK, + data: responseData{ + "message": "Hello, World!", + }, + headers: nil, + expected: "{\n\t\"message\": \"Hello, World!\"\n}\n", + }, + { + name: "JSON response with custom headers", + status: http.StatusCreated, + data: responseData{ + "id": 1, + "name": "Test", + }, + headers: http.Header{ + "X-Custom-Header": []string{"CustomValue"}, + }, + expected: "{\n\t\"id\": 1,\n\t\"name\": \"Test\"\n}\n", + }, + { + name: "Empty JSON response", + status: http.StatusNoContent, + data: responseData{}, + headers: nil, + expected: "{}\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + err := appliance.writeJSON(w, tt.status, tt.data, tt.headers) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if diff := cmp.Diff(tt.status, w.Code); diff != "" { + t.Errorf("status mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff("application/json", w.Header().Get("Content-Type")); diff != "" { + t.Errorf("Content-Type mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.expected, w.Body.String()); diff != "" { + t.Errorf("body mismatch (-want +got):\n%s", diff) + } + + if tt.headers != nil { + for key, value := range tt.headers { + if diff := cmp.Diff(value, w.Header()[key]); diff != "" { + t.Errorf("header %q mismatch (-want +got):\n%s", key, diff) + } + } + } + }) + } +} + +func TestWriteJSONError(t *testing.T) { + appliance := &Appliance{ + logger: log.NoOp(), + } + + w := httptest.NewRecorder() + data := responseData{ + "data": make(chan int), + } + + err := appliance.writeJSON(w, http.StatusOK, data, nil) + + if err == nil { + t.Error("expected an error, got nil") + } + + expectedErrSubstring := "json: unsupported type: chan int" + if diff := cmp.Diff(true, strings.Contains(err.Error(), expectedErrSubstring)); diff != "" { + t.Errorf("error message mismatch (-want +got):\n%s", diff) + } +} diff --git a/internal/appliance/k8senvtest/BUILD.bazel b/internal/appliance/k8senvtest/BUILD.bazel index eb9d7b93047ed..9a11f8c515e41 100644 --- a/internal/appliance/k8senvtest/BUILD.bazel +++ b/internal/appliance/k8senvtest/BUILD.bazel @@ -2,13 +2,18 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "k8senvtest", - srcs = ["envtest.go"], + srcs = [ + "envtest.go", + "namespaces.go", + ], importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest", visibility = ["//:__subpackages__"], deps = [ "//lib/errors", "@com_github_go_logr_logr//:logr", "@io_bazel_rules_go//go/runfiles:go_default_library", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", "@io_k8s_client_go//kubernetes/scheme", "@io_k8s_client_go//rest", "@io_k8s_sigs_controller_runtime//:controller-runtime", diff --git a/internal/appliance/k8senvtest/README.md b/internal/appliance/k8senvtest/README.md new file mode 100644 index 0000000000000..12c2878549a86 --- /dev/null +++ b/internal/appliance/k8senvtest/README.md @@ -0,0 +1,17 @@ +# k8senvtest + +A wrapper package for sigs.k8s.io/controller-runtime/pkg/envtest. Has +compatibility with our bazel setup. Any package that makes us of this one should +add the following to the go_test directive in its BUILD.bazel: + +```starlark +data = [ + "//dev/tools:kubebuilder-assets", +], +env = { + "KUBEBUILDER_ASSET_PATHS": "$(rlocationpaths //dev/tools:kubebuilder-assets)", +}, +``` + +And this should just work out of the box. See consumers of this package for +examples on how to use it, including safe teardown. diff --git a/internal/appliance/k8senvtest/envtest.go b/internal/appliance/k8senvtest/envtest.go index 5fcaa1293a663..5f2bdb970a3e4 100644 --- a/internal/appliance/k8senvtest/envtest.go +++ b/internal/appliance/k8senvtest/envtest.go @@ -119,3 +119,11 @@ func kubebuilderAssetPathLocalDev() (string, error) { } return strings.TrimSpace(envtestOut.String()), nil } + +func NewNoopReconciler(mgr ctrl.Manager) KubernetesController { + return noopReconicler{} +} + +type noopReconicler struct{} + +func (noopReconicler) SetupWithManager(_ ctrl.Manager) error { return nil } diff --git a/internal/appliance/k8senvtest/namespaces.go b/internal/appliance/k8senvtest/namespaces.go new file mode 100644 index 0000000000000..aa31e10618cee --- /dev/null +++ b/internal/appliance/k8senvtest/namespaces.go @@ -0,0 +1,34 @@ +package k8senvtest + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// In order to be able to run tests in isolation, we can make use of namespaces +// with a random suffix. We don't need to delete these, all data will be +// desstroyed on envtest teardown. +func NewRandomNamespace(prefix string) (*corev1.Namespace, error) { + slug, err := randomSlug() + if err != nil { + return nil, err + } + name := fmt.Sprintf("%s-%s", prefix, slug) + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, nil +} + +func randomSlug() (string, error) { + buf := make([]byte, 3) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} diff --git a/internal/appliance/maintenance/README.md b/internal/appliance/maintenance/README.md deleted file mode 100644 index 309e8f319018f..0000000000000 --- a/internal/appliance/maintenance/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# Operator Maintenance UI - -## Components - -This project contains the following components: - -### Maintenance UI - -A React + Material UI application that communicates with the Operator and gathers data and display status. - -Features: - -- Installation -- Health & Actions -- Upgrade - -### Mock Operator API - -In the [mock-api](./mock-api/) folder, a Go Server application that implements the Operator API companion to the Maintenance UI. - -#### Mock Operator Debug Bar API - -We also implement some test APIs to enable controlling the Mock Operator from the Maitenance UI. - -## Running Locally (Developer Mode) - -1. Run the go application in the `mock-api` folder: - - ``` - $ cd mock-api - $ go run ./cmd - ``` - -2. Run the Maitenance UI: - - ``` - $ pnpm run dev - ``` - -## Building Images - -``` -$ cd build -$ make -``` - -It will: - -1. Build frontend and backend distributables -2. Build docker images -3. Push images to the container registry -4. Update the Helm chart with the appropriate registry image versions - -## Helm Chart - -### Preparing the Helm Chart - -No action. This step is automated by the image build step. - -### Packaging the Helm Chart - -TBD - -### Installing the Helm Chart - -1. Have a Kubernetes cluster configured and available at the command line -2. Test you can access the cluster by running: `kubectl get pods` -3. Install the Helm chart: - - ``` - $ helm install operator ./helm - ``` - - Installer will create the `sourcegraph` namespace - -4. Execute the commands output by the installer to get the address of - the maintenance UI - -### Launching the Maintenance UI - -Once the data provided by the install step is available, -IP address + maintenance password, open the maintenance UI in your -browser and follow along the wizard. - -### Run debug console - -Maintenance UI has a debug console that can be used to control flows in the maintenance UI, -to enable set `debugbar: true` in your browser local storage. diff --git a/internal/appliance/maintenance/backend/api/BUILD.bazel b/internal/appliance/maintenance/backend/api/BUILD.bazel index 867ff159adca5..36fd087a6ebd0 100644 --- a/internal/appliance/maintenance/backend/api/BUILD.bazel +++ b/internal/appliance/maintenance/backend/api/BUILD.bazel @@ -13,8 +13,5 @@ go_library( ], importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/api", visibility = ["//:__subpackages__"], - deps = [ - "//internal/appliance/maintenance/backend/operator", - "@com_github_gorilla_mux//:mux", - ], + deps = ["@com_github_gorilla_mux//:mux"], ) diff --git a/internal/appliance/maintenance/backend/api/install.go b/internal/appliance/maintenance/backend/api/install.go index a3f179cc68fe1..68265f4504121 100644 --- a/internal/appliance/maintenance/backend/api/install.go +++ b/internal/appliance/maintenance/backend/api/install.go @@ -3,19 +3,17 @@ package api import ( "fmt" "net/http" - - "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator" ) var installError string = "" -var installTasks []operator.Task = createInstallTasks() +var installTasks []Task = createInstallTasks() var installVersion string = "" type InstallProgress struct { - Version string `json:"version"` - Progress int `json:"progress"` - Error string `json:"error"` - Tasks []operator.Task `json:"tasks"` + Version string `json:"version"` + Progress int `json:"progress"` + Error string `json:"error"` + Tasks []Task `json:"tasks"` } func InstallProgressHandler(w http.ResponseWriter, r *http.Request) { diff --git a/internal/appliance/maintenance/backend/api/stage.go b/internal/appliance/maintenance/backend/api/stage.go index ad46053daaf1d..58f2677d89b18 100644 --- a/internal/appliance/maintenance/backend/api/stage.go +++ b/internal/appliance/maintenance/backend/api/stage.go @@ -8,8 +8,6 @@ import ( "net/http" "os" "time" - - "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator" ) var maintenanceEndpoint = os.Getenv("MAINTENANCE_ENDPOINT") @@ -21,6 +19,26 @@ func init() { } } +type status struct { + Stage Stage `json:"stage"` + CurrentVersion *string `json:"version"` // current version, nil if not installed + NextVersion *string `json:"nextVersion"` // version being installed/upgraded nil if not being installed/upgraded + Tasks []Task `json:"tasks"` + Errors []string `json:"errors"` +} + +type Stage string + +const ( + StageUnknown Stage = "unknown" + StageIdle Stage = "idle" + StageInstall Stage = "install" + StageInstalling Stage = "installing" + StageUpgrading Stage = "upgrading" + StageWaitingForAdmin Stage = "wait-for-admin" + StageRefresh Stage = "refresh" +) + type Feature struct { Name string `json:"name"` Enabled bool `json:"enabled"` @@ -40,7 +58,7 @@ type StageResponse struct { var epoch = time.Unix(0, 0) -var currentStage operator.Stage = operator.StageInstall +var currentStage Stage = StageInstall var switchToAdminTime time.Time = epoch func init() { @@ -55,17 +73,17 @@ func StageHandler(w http.ResponseWriter, r *http.Request) { switch status { case "installing": - currentStage = operator.StageInstalling + currentStage = StageInstalling case "ready": fmt.Println("ready!", switchToAdminTime, currentStage) if switchToAdminTime == time.Unix(0, 0) { - if currentStage != operator.StageRefresh && currentStage != operator.StageWaitingForAdmin { + if currentStage != StageRefresh && currentStage != StageWaitingForAdmin { switchToAdminTime = time.Now().Add(5 * time.Second) } } else { if time.Now().After(switchToAdminTime) { switchToAdminTime = epoch - currentStage = operator.StageWaitingForAdmin + currentStage = StageWaitingForAdmin } } case "unknown": @@ -77,8 +95,8 @@ func StageHandler(w http.ResponseWriter, r *http.Request) { } switch currentStage { - case operator.StageRefresh: - currentStage = operator.StageUnknown + case StageRefresh: + currentStage = StageUnknown } fmt.Println("Sending current stage", result) @@ -92,12 +110,12 @@ func SetStageHandlerForTesting(w http.ResponseWriter, r *http.Request) { receiveJson(w, r, &request) fmt.Println("Setting stage to", request.Stage) - currentStage = operator.Stage(request.Stage) + currentStage = Stage(request.Stage) fmt.Println(installTasks) switch currentStage { - case operator.StageInstalling: + case StageInstalling: installError = "" installTasks = createInstallTasks() installVersion = request.Data @@ -107,7 +125,7 @@ func SetStageHandlerForTesting(w http.ResponseWriter, r *http.Request) { installError = err.Error() } }() - case operator.StageUpgrading: + case StageUpgrading: installError = "" installTasks = createFakeUpgradeTasks() installVersion = request.Data diff --git a/internal/appliance/maintenance/backend/api/task.go b/internal/appliance/maintenance/backend/api/task.go index c5a7513839d8e..6383a40a9a1df 100644 --- a/internal/appliance/maintenance/backend/api/task.go +++ b/internal/appliance/maintenance/backend/api/task.go @@ -3,16 +3,24 @@ package api import ( "math/rand" "time" - - "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator" ) const InstallTaskWaitForCluster = 0 const InstallTaskSetup = 1 const InstallTaskStart = 2 -func createInstallTasks() []operator.Task { - return []operator.Task{ +type Task struct { + Title string `json:"title"` + Description string `json:"description"` + Started bool `json:"started"` + Finished bool `json:"finished"` + Weight int `json:"weight"` + Progress int `json:"progress"` + LastUpdate time.Time `json:"lastUpdate"` +} + +func createInstallTasks() []Task { + return []Task{ { Title: "Warming up", Description: "Setting up basic resources", @@ -37,8 +45,8 @@ func createInstallTasks() []operator.Task { } } -func createFakeUpgradeTasks() []operator.Task { - return []operator.Task{ +func createFakeUpgradeTasks() []Task { + return []Task{ { Title: "Upgrade", Description: "Upgrade Sourcegraph", @@ -56,8 +64,8 @@ func createFakeUpgradeTasks() []operator.Task { } } -func progressTasks(tasks []operator.Task) []operator.Task { - var result []operator.Task +func progressTasks(tasks []Task) []Task { + var result []Task var previousStarted bool = true var previousFinished bool = true @@ -76,8 +84,8 @@ func progressTasks(tasks []operator.Task) []operator.Task { return result } -func calculateProgress() ([]operator.Task, int) { - var result []operator.Task +func calculateProgress() ([]Task, int) { + var result []Task var taskWeights int = 0 for _, t := range installTasks { diff --git a/internal/appliance/maintenance/backend/cmd/BUILD.bazel b/internal/appliance/maintenance/backend/cmd/BUILD.bazel deleted file mode 100644 index 3d13575255169..0000000000000 --- a/internal/appliance/maintenance/backend/cmd/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") - -go_library( - name = "cmd_lib", - srcs = ["main.go"], - importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/cmd", - visibility = ["//visibility:private"], - deps = ["//internal/appliance/maintenance/backend/api"], -) - -go_binary( - name = "cmd", - embed = [":cmd_lib"], - visibility = ["//:__subpackages__"], -) diff --git a/internal/appliance/maintenance/backend/cmd/main.go b/internal/appliance/maintenance/backend/cmd/main.go deleted file mode 100644 index 67bf2591a52ec..0000000000000 --- a/internal/appliance/maintenance/backend/cmd/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/api" -) - -func main() { - server := api.New() - fmt.Println("Starting mock API server") - server.Run() -} diff --git a/internal/appliance/maintenance/backend/operator/BUILD.bazel b/internal/appliance/maintenance/backend/operator/BUILD.bazel deleted file mode 100644 index f93ff3308425c..0000000000000 --- a/internal/appliance/maintenance/backend/operator/BUILD.bazel +++ /dev/null @@ -1,11 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "operator", - srcs = [ - "manage.go", - "task.go", - ], - importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/maintenance/backend/operator", - visibility = ["//:__subpackages__"], -) diff --git a/internal/appliance/maintenance/backend/operator/manage.go b/internal/appliance/maintenance/backend/operator/manage.go deleted file mode 100644 index e729fb9061a66..0000000000000 --- a/internal/appliance/maintenance/backend/operator/manage.go +++ /dev/null @@ -1,58 +0,0 @@ -package operator - -type K8sManager interface { - Status() *status - Install(version string) error - Upgrade(version string) error -} - -func New() K8sManager { - return &manager{} -} - -type status struct { - Stage Stage `json:"stage"` - CurrentVersion *string `json:"version"` // current version, nil if not installed - NextVersion *string `json:"nextVersion"` // version being installed/upgraded nil if not being installed/upgraded - Tasks []Task `json:"tasks"` - Errors []string `json:"errors"` -} - -type Stage string - -const ( - StageUnknown Stage = "unknown" - StageIdle Stage = "idle" - StageInstall Stage = "install" - StageInstalling Stage = "installing" - StageUpgrading Stage = "upgrading" - StageWaitingForAdmin Stage = "wait-for-admin" - StageRefresh Stage = "refresh" -) - -type manager struct{} - -// Asks the Operator to kick off a new installation of the specified version. -// -// Returns an error if the installation was not successful, -// if the version is not supported, or a version is already installed. -// -// Once the request is accepted, the status can be tracked via the Status() method. -func (*manager) Install(version string) error { - panic("unimplemented") -} - -// Asks the Operator to upgrade to the specified version. -// -// Returns an error if the upgrade was not successful, -// if the version is not supported, or if there's no existing version installed. -// -// Once the request is accepted, the status can be tracked via the Status() method. -func (*manager) Upgrade(version string) error { - panic("unimplemented") -} - -// Returns the current status of the Operator. -func (*manager) Status() *status { - panic("unimplemented") -} diff --git a/internal/appliance/maintenance/backend/operator/task.go b/internal/appliance/maintenance/backend/operator/task.go deleted file mode 100644 index d220426c5806d..0000000000000 --- a/internal/appliance/maintenance/backend/operator/task.go +++ /dev/null @@ -1,13 +0,0 @@ -package operator - -import "time" - -type Task struct { - Title string `json:"title"` - Description string `json:"description"` - Started bool `json:"started"` - Finished bool `json:"finished"` - Weight int `json:"weight"` - Progress int `json:"progress"` - LastUpdate time.Time `json:"lastUpdate"` -} diff --git a/internal/appliance/maintenance/demo.html b/internal/appliance/maintenance/demo.html deleted file mode 100644 index 634b87640d42c..0000000000000 --- a/internal/appliance/maintenance/demo.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - -
- -

Appliance

-
-
-

Pre-Requisites

-
    -
  1. A Kubernetes Cluster (any kind: k3s, minicube, GKE, EKS, etc)
  2. -
  3. -
    kubectl
    - configured in your command line with credentials to the cluster -
  4. -
  5. - Kubernetes context set to the namespace you want to create - Sourcegraph. -

    - If you don't ever set, it will install in the - default namespace -

    -
  6. -
-

Install

-

- This is the only cluster piece required. From this point on, all - installation happens guided by the Operator: -

-
-kubectl apply -f https://storage.googleapis.com/merge-appliance-demo/v0.0.5999925/bundle.yaml
-

- We will need to get the IP address of the Appliance, as well the - maintenance password. -

-

The steps below help you get those values...

- -

Get Frontend Address

-
kubectl get svc operator-ui --watch
-

Once the external IP address is available, you visit that page.

- -
-% kubectl get svc operator-ui
-NAME          TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)        AGE
-operator-ui   LoadBalancer   10.92.6.197   34.71.130.103   80:31883/TCP   10h
-                                           ⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑⇑
-                                           this address
- -

Navigate to the Appliance Page

-
http://<ip-address-above>/
- -

Get the Maintenance Password

-
-kubectl get secret operator-api -o json \
-    | jq '{name: .metadata.name,data: .data|map_values(@base64d)}'
- -

Example output:

- -
-{
-  "name": "operator-api",
-  "data": {
-    "MAINTENANCE_PASSWORD": "password-is-here"
-  }
-}
- -

Install

-
    -
  1. Follow the wizard
  2. -
  3. - Once the installation is complete, you will see a "Wait for Admin to - Return" -

    - This step is to avoid exposing the admin UI before creating a user, - allowing, for example, the administrator to leave the - installation/upgrade/maintenance going and walk away from the - computer. -

    -
  4. -
  5. Press the Launch button and the Admin UI will start
  6. -
- -

Teardown

-

This will DELETE ALL DATA:

-
-kubectl delete -f https://storage.googleapis.com/merge-appliance-demo/v0.0.5999925/bundle.yaml
-kubectl delete pvc --all
-
- - diff --git a/internal/appliance/maintenance/helm/.helmignore b/internal/appliance/maintenance/helm/.helmignore deleted file mode 100644 index 0e8a0eb36f4ca..0000000000000 --- a/internal/appliance/maintenance/helm/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/internal/appliance/maintenance/helm/Chart.yaml b/internal/appliance/maintenance/helm/Chart.yaml deleted file mode 100644 index d7cf56fc48b15..0000000000000 --- a/internal/appliance/maintenance/helm/Chart.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v2 -name: operator -description: Operator Maintenance UI - -type: application - -version: v0.0.5999860 -appVersion: "v0.0.5999860" diff --git a/internal/appliance/maintenance/helm/templates/NOTES.txt b/internal/appliance/maintenance/helm/templates/NOTES.txt deleted file mode 100644 index d7aae41a27223..0000000000000 --- a/internal/appliance/maintenance/helm/templates/NOTES.txt +++ /dev/null @@ -1,51 +0,0 @@ -============================================================== - ____ ____ _ _ ____ ____ ____ ____ ____ ____ ___ _ _ - [__ | | | | |__/ | |___ | __ |__/ |__| |__] |__| - ___] |__| |__| | \ |___ |___ |__] | \ | | | | | - - ____ ___ ____ ____ ____ ___ ____ ____ - [__] |--' |=== |--< |--| | [__] |--< - - Version: {{ .Chart.Version }} - --------------------------------------------------------------- - -Thanks for installing the Operator UI. - -To check if the operator is running, try: - - $ helm status {{ .Release.Name }} - $ helm get all {{ .Release.Name }} - --------------------------------------------------------------- - -The maintenance (and installation) interface is -available at: - - $ echo -n 'http://' \ - && kubectl get service operator-ui \ - -o jsonpath='{.status.loadBalancer.ingress[0].ip}' \ - --namespace {{ .Values.namespace }} && echo - -If the result is simply `http://`, then it means the service -is not fully provisioned yet. Either wait or monitor the -output of this command: - - $ kubectl get service operator-ui \ - --namespace {{ .Values.namespace }} - -The `EXTERNAL-IP` field will be either `` or an IP -address. - --------------------------------------------------------------- - -To access the interface, you will need the maintenance -password: - - $ echo -n 'Password: ' \ - && kubectl get secret operator-api \ - -o jsonpath='{.data.MAINTENANCE_PASSWORD}' \ - --namespace {{ .Values.namespace }} \ - | base64 -d && echo - --------------------------------------------------------------- diff --git a/internal/appliance/maintenance/helm/templates/api-deploy.yaml b/internal/appliance/maintenance/helm/templates/api-deploy.yaml deleted file mode 100644 index f865928547c1f..0000000000000 --- a/internal/appliance/maintenance/helm/templates/api-deploy.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: operator-api - namespace: {{ .Values.namespace }} - labels: - app: operator-api -spec: - replicas: 1 - selector: - matchLabels: - app: operator-api - template: - metadata: - labels: - app: operator-api - spec: - containers: - - name: operator-api - image: {{ .Values.registry }}/{{ .Values.api.image }} - ports: - - containerPort: 80 - env: - - - name: API_ENDPOINT - value: 'maintenance.{{ .Values.namespace }}.svc.cluster.local' - - name: MAINTENANCE_PASSWORD - valueFrom: - secretKeyRef: - name: operator-api - key: MAINTENANCE_PASSWORD diff --git a/internal/appliance/maintenance/helm/templates/api-secret.yaml b/internal/appliance/maintenance/helm/templates/api-secret.yaml deleted file mode 100644 index 3537b46087265..0000000000000 --- a/internal/appliance/maintenance/helm/templates/api-secret.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: operator-api - namespace: {{ .Values.namespace }} -data: - {{- $secretObj := (lookup "v1" "Secret" .Values.namespace "operator-api") | default dict }} - {{- $secretData := (get $secretObj "data") | default dict }} - {{- $secret := (get $secretData "MAINTENANCE_PASSWORD") | default (randAlphaNum 15 | b64enc) }} - MAINTENANCE_PASSWORD: {{ $secret | quote }} diff --git a/internal/appliance/maintenance/helm/templates/api-service.yaml b/internal/appliance/maintenance/helm/templates/api-service.yaml deleted file mode 100644 index 5263e183bb2de..0000000000000 --- a/internal/appliance/maintenance/helm/templates/api-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: operator-api - namespace: {{ .Values.namespace }} -spec: - selector: - app: operator-api - ports: - - protocol: TCP - port: 80 - targetPort: 80 - type: ClusterIP diff --git a/internal/appliance/maintenance/helm/templates/namespace.yaml b/internal/appliance/maintenance/helm/templates/namespace.yaml deleted file mode 100644 index 77db5f9f65515..0000000000000 --- a/internal/appliance/maintenance/helm/templates/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: {{ .Values.namespace }} diff --git a/internal/appliance/maintenance/helm/templates/ui-deploy.yaml b/internal/appliance/maintenance/helm/templates/ui-deploy.yaml deleted file mode 100644 index d589dc3dd69eb..0000000000000 --- a/internal/appliance/maintenance/helm/templates/ui-deploy.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: operator-ui - namespace: {{ .Values.namespace }} - labels: - app: operator-ui -spec: - replicas: 1 - selector: - matchLabels: - app: operator-ui - template: - metadata: - labels: - app: operator-ui - spec: - containers: - - name: operator-ui - image: {{ .Values.registry }}/{{ .Values.webui.image }} - ports: - - containerPort: 80 - env: - - name: API_ENDPOINT - value: 'http://operator-api.{{ .Values.namespace }}.svc.cluster.local' diff --git a/internal/appliance/maintenance/helm/templates/ui-service.yaml b/internal/appliance/maintenance/helm/templates/ui-service.yaml deleted file mode 100644 index 02daaccbc45e6..0000000000000 --- a/internal/appliance/maintenance/helm/templates/ui-service.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: operator-ui - namespace: {{ .Values.namespace }} -spec: - selector: - app: operator-ui - ports: - - protocol: TCP - port: 80 - targetPort: 80 - type: LoadBalancer diff --git a/internal/appliance/maintenance/helm/values.yaml b/internal/appliance/maintenance/helm/values.yaml deleted file mode 100644 index acd934603ffea..0000000000000 --- a/internal/appliance/maintenance/helm/values.yaml +++ /dev/null @@ -1,9 +0,0 @@ -namespace: sourcegraph - -registry: us-central1-docker.pkg.dev/sg-infra-release-merge-feb2024/merge-workshop-feb2024 - -webui: - image: operator-ui:v0.0.5999860 - -api: - image: operator-api:v0.0.5999860 diff --git a/internal/appliance/reconciler/BUILD.bazel b/internal/appliance/reconciler/BUILD.bazel index 04cace9a34cf0..39b061b3926c3 100644 --- a/internal/appliance/reconciler/BUILD.bazel +++ b/internal/appliance/reconciler/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "indexed_search.go", "jaeger.go", "kubernetes.go", + "otel_agent.go", "pgsql.go", "precise_code_intel.go", "prometheus.go", @@ -21,6 +22,7 @@ go_library( "redis.go", "repo_updater.go", "searcher.go", + "secret_management.go", "symbols.go", "syntect.go", "worker.go", @@ -54,9 +56,11 @@ go_library( "@io_k8s_apimachinery//pkg/runtime", "@io_k8s_apimachinery//pkg/types", "@io_k8s_apimachinery//pkg/util/intstr", + "@io_k8s_apimachinery//pkg/util/strategicpatch", "@io_k8s_client_go//tools/record", "@io_k8s_sigs_controller_runtime//:controller-runtime", "@io_k8s_sigs_controller_runtime//pkg/client", + "@io_k8s_sigs_controller_runtime//pkg/client/apiutil", "@io_k8s_sigs_controller_runtime//pkg/log", "@io_k8s_sigs_controller_runtime//pkg/predicate", "@io_k8s_sigs_controller_runtime//pkg/reconcile", @@ -83,6 +87,7 @@ go_test( "helpers_test.go", "indexed_search_test.go", "jaeger_test.go", + "otel_agent_test.go", "pgsql_test.go", "precise_code_intel_test.go", "prometheus_test.go", @@ -107,15 +112,21 @@ go_test( "//internal/appliance/config", "//internal/appliance/k8senvtest", "//internal/appliance/yaml", + "//internal/k8s/resource/ingress", + "//lib/pointers", "@com_github_life4_genesis//slices", "@com_github_sourcegraph_log//logtest", "@com_github_sourcegraph_log_logr//:logr", + "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", "@com_github_stretchr_testify//suite", "@io_k8s_api//core/v1:core", + "@io_k8s_api//networking/v1:networking", "@io_k8s_api//rbac/v1:rbac", "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_apimachinery//pkg/runtime", "@io_k8s_apimachinery//pkg/runtime/schema", + "@io_k8s_apimachinery//pkg/util/intstr", "@io_k8s_client_go//kubernetes", "@io_k8s_client_go//rest", "@io_k8s_sigs_controller_runtime//:controller-runtime", diff --git a/internal/appliance/reconciler/frontend.go b/internal/appliance/reconciler/frontend.go index 3a8366f5e1dc4..46343825d3430 100644 --- a/internal/appliance/reconciler/frontend.go +++ b/internal/appliance/reconciler/frontend.go @@ -7,9 +7,12 @@ import ( corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "github.com/sourcegraph/sourcegraph/internal/appliance/config" "github.com/sourcegraph/sourcegraph/internal/k8s/resource/container" @@ -22,12 +25,9 @@ import ( "github.com/sourcegraph/sourcegraph/internal/k8s/resource/serviceaccount" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/pointers" -) -const ( - pgsqlSecretName = "pgsql-auth" - codeInsightsDBSecretName = "codeinsights-db-auth" - codeIntelDBSecretName = "codeintel-db-auth" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/strategicpatch" ) func (r *Reconciler) reconcileFrontend(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { @@ -114,6 +114,26 @@ func (r *Reconciler) reconcileFrontendDeployment(ctx context.Context, sg *config } template := pod.NewPodTemplate("sourcegraph-frontend", cfg) + dbConnSpecs, err := r.getDBSecrets(ctx, sg) + if err != nil { + return err + } + dbConnHash, err := configHash(dbConnSpecs) + if err != nil { + return err + } + template.Template.ObjectMeta.Annotations["checksum/auth"] = dbConnHash + + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + template.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash + template.Template.Spec.Containers = []corev1.Container{ctr} template.Template.Spec.Volumes = []corev1.Volume{pod.NewVolumeEmptyDir("home-dir")} template.Template.Spec.ServiceAccountName = "sourcegraph-frontend" @@ -142,27 +162,65 @@ func (r *Reconciler) reconcileFrontendDeployment(ctx context.Context, sg *config dep := deployment.NewDeployment("sourcegraph-frontend", sg.Namespace, sg.Spec.RequestedVersion) dep.Spec.Replicas = &cfg.Replicas dep.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ - MaxSurge: pointers.Ptr(intstr.FromInt(2)), - MaxUnavailable: pointers.Ptr(intstr.FromInt(0)), + MaxSurge: pointers.Ptr(intstr.FromInt32(2)), + MaxUnavailable: pointers.Ptr(intstr.FromInt32(0)), } dep.Spec.Template = template.Template - return reconcileObject(ctx, r, cfg, &dep, &appsv1.Deployment{}, sg, owner) + ifChanged := struct { + config.FrontendSpec + DBConnSpecs + RedisConnSpecs + }{ + FrontendSpec: cfg, + DBConnSpecs: dbConnSpecs, + RedisConnSpecs: redisConnSpecs, + } + + return reconcileObject(ctx, r, ifChanged, &dep, &appsv1.Deployment{}, sg, owner) } func (r *Reconciler) reconcileFrontendService(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { name := "sourcegraph-frontend" cfg := sg.Spec.Frontend + logger := log.FromContext(ctx).WithValues("kind", "from ingress creation") + namespacedName := types.NamespacedName{Namespace: sg.Namespace, Name: name} + existingObj := &corev1.Service{} + if err := r.Client.Get(ctx, namespacedName, existingObj); err != nil { + // If we don't find an object, create one from the spec + if kerrors.IsNotFound(err) { + svc := service.NewService(name, sg.Namespace, cfg) + svc.Spec.Ports = []corev1.ServicePort{ + {Name: "http", Port: 30080, TargetPort: intstr.FromString("http")}, + } + svc.Spec.Selector = map[string]string{ + "app": "sourcegraph-appliance", + } + + config.MarkObjectForAdoption(&svc) + return reconcileObject(ctx, r, cfg, &svc, &corev1.Service{}, sg, owner) + } + logger.Error(err, "unexpected error getting object") + return err + } - svc := service.NewService(name, sg.Namespace, cfg) - svc.Spec.Ports = []corev1.ServicePort{ - {Name: "http", Port: 30080, TargetPort: intstr.FromString("http")}, + // If we found an object, we only want to change configmap-specified things, + // and certain defaults such as the prometheus port. + svcChanges := &corev1.Service{} + svcChanges.SetAnnotations(map[string]string{ + "prometheus.io/port": "6060", + "sourcegraph.prometheus/scrape": "true", + }) + config.MarkObjectForAdoption(svcChanges) + newObj, err := MergeK8sObjects(existingObj, svcChanges) + if err != nil { + return errors.Wrap(err, "merging objects") } - svc.Spec.Selector = map[string]string{ - "app": name, + newSvc, ok := newObj.(*corev1.Service) + if !ok { + return errors.Wrap(err, "asserting type") } - - return reconcileObject(ctx, r, cfg, &svc, &corev1.Service{}, sg, owner) + return reconcileObject(ctx, r, sg.Spec.Frontend, newSvc, &corev1.Service{}, sg, owner) } func (r *Reconciler) reconcileFrontendServiceInternal(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { @@ -228,43 +286,75 @@ func (r *Reconciler) reconcileFrontendRoleBinding(ctx context.Context, sg *confi func (r *Reconciler) reconcileFrontendIngress(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { name := "sourcegraph-frontend" cfg := sg.Spec.Frontend - ingress := ingress.NewIngress(name, sg.Namespace) - if cfg.Ingress == nil { - return r.ensureObjectDeleted(ctx, &ingress) - } + logger := log.FromContext(ctx).WithValues("kind", "from ingress creation") + namespacedName := types.NamespacedName{Namespace: sg.Namespace, Name: name} + existingObj := &netv1.Ingress{} + if err := r.Client.Get(ctx, namespacedName, existingObj); err != nil { + // If we don't find an object, create one from the spec + if kerrors.IsNotFound(err) { + ingress := ingress.NewIngress(name, sg.Namespace) + if cfg.Ingress == nil { + return ensureObjectDeleted(ctx, r, owner, &ingress) + } + + ingress.SetAnnotations(cfg.Ingress.Annotations) + + if cfg.Ingress.TLSSecret != "" { + ingress.Spec.TLS = []netv1.IngressTLS{{ + Hosts: []string{cfg.Ingress.Host}, + SecretName: cfg.Ingress.TLSSecret, + }} + } + + ingress.Spec.Rules = []netv1.IngressRule{{ + Host: cfg.Ingress.Host, + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{{ + Path: "/", + PathType: pointers.Ptr(netv1.PathTypePrefix), + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: name, + Port: netv1.ServiceBackendPort{ + Number: 30080, + }, + }, + }, + }}, + }, + }, + }} + + ingress.Spec.IngressClassName = cfg.Ingress.IngressClassName - ingress.SetAnnotations(cfg.Ingress.Annotations) + config.MarkObjectForAdoption(&ingress) + return reconcileObject(ctx, r, sg.Spec.Frontend, &ingress, &netv1.Ingress{}, sg, owner) + } + logger.Error(err, "unexpected error getting object") + return err + } + //Otherwise we found an object and only want to craft changes + cfgIngress := ingress.NewIngress(name, sg.Namespace) + cfgIngress.SetAnnotations(cfg.Ingress.Annotations) if cfg.Ingress.TLSSecret != "" { - ingress.Spec.TLS = []netv1.IngressTLS{{ + cfgIngress.Spec.TLS = []netv1.IngressTLS{{ Hosts: []string{cfg.Ingress.Host}, SecretName: cfg.Ingress.TLSSecret, }} } - - ingress.Spec.Rules = []netv1.IngressRule{{ - Host: cfg.Ingress.Host, - IngressRuleValue: netv1.IngressRuleValue{ - HTTP: &netv1.HTTPIngressRuleValue{ - Paths: []netv1.HTTPIngressPath{{ - Path: "/", - PathType: pointers.Ptr(netv1.PathTypePrefix), - Backend: netv1.IngressBackend{ - Service: &netv1.IngressServiceBackend{ - Name: name, - Port: netv1.ServiceBackendPort{ - Number: 30080, - }, - }, - }, - }}, - }, - }, - }} - - ingress.Spec.IngressClassName = cfg.Ingress.IngressClassName - - return reconcileObject(ctx, r, sg.Spec.Frontend, &ingress, &netv1.Ingress{}, sg, owner) + cfgIngress.Spec.IngressClassName = cfg.Ingress.IngressClassName + config.MarkObjectForAdoption(&cfgIngress) + newObj, err := MergeK8sObjects(existingObj, &cfgIngress) + if err != nil { + return errors.Wrap(err, "merging objects") + } + newObjAsIngress, ok := newObj.(*netv1.Ingress) + if !ok { + return errors.Wrap(err, "asserting type") + } + return reconcileObject(ctx, r, sg.Spec.Frontend, newObjAsIngress, &netv1.Ingress{}, sg, owner) } func frontendEnvVars(sg *config.Sourcegraph) []corev1.EnvVar { @@ -302,3 +392,33 @@ func dbAuthVars() []corev1.EnvVar { container.NewEnvVarSecretKeyRef("CODEINSIGHTS_PGUSER", codeInsightsDBSecretName, "user"), } } + +// MergeK8sObjects merges a Kubernetes object that already exists within the cluster +// with an existing Kubernetes object definition. +func MergeK8sObjects(existingObj client.Object, newObject client.Object) (client.Object, error) { + // Convert existing object to unstructured + existingUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(existingObj) + if err != nil { + return nil, errors.Newf("failed to convert existing object to unstructured: %w", err) + } + + newUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newObject) + if err != nil { + return nil, errors.Newf("failed to convert new object to unstructured: %w", err) + } + + // Merge the objects using strategic merge patch + mergedUnstructured, err := strategicpatch.StrategicMergeMapPatch(existingUnstructured, newUnstructured, existingObj) + if err != nil { + return nil, errors.Newf("failed to merge objects: %w", err) + } + + // Convert the merged unstructured object back to the original type + mergedObj := existingObj.DeepCopyObject().(client.Object) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(mergedUnstructured, mergedObj) + if err != nil { + return nil, errors.Newf("failed to convert merged object from unstructured: %w", err) + } + + return mergedObj, nil +} diff --git a/internal/appliance/reconciler/frontend_test.go b/internal/appliance/reconciler/frontend_test.go index 29d3faae53b7f..405c60b06f8b8 100644 --- a/internal/appliance/reconciler/frontend_test.go +++ b/internal/appliance/reconciler/frontend_test.go @@ -1,5 +1,22 @@ package reconciler +import ( + "fmt" + "maps" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/ingress" + "github.com/sourcegraph/sourcegraph/lib/pointers" +) + func (suite *ApplianceTestSuite) TestDeployFrontend() { for _, tc := range []struct { name string @@ -16,3 +33,324 @@ func (suite *ApplianceTestSuite) TestDeployFrontend() { }) } } + +func (suite *ApplianceTestSuite) TestAdoptsHelmProvisionedFrontendResources() { + namespace, err := k8senvtest.NewRandomNamespace("test-appliance") + suite.Require().NoError(err) + _, err = suite.k8sClient.CoreV1().Namespaces().Create(suite.ctx, namespace, metav1.CreateOptions{}) + suite.Require().NoError(err) + testService := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sourcegraph-frontend", + Namespace: namespace.Name, + Labels: map[string]string{ + "app": "sourcegraph-frontend", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Name: "http", Port: 30080, TargetPort: intstr.FromString("http")}}, + Selector: map[string]string{"app": "sourcegraph-appliance"}, + }, + } + _, err = suite.k8sClient.CoreV1().Services(namespace.Name).Create(suite.ctx, &testService, metav1.CreateOptions{}) + suite.Require().NoError(err) + + testIngress := ingress.NewIngress("sourcegraph-frontend", namespace.Name) + testIngress.Spec.Rules = []netv1.IngressRule{{ + Host: "an-existing-hostname.com", + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{{ + Path: "/", + PathType: pointers.Ptr(netv1.PathTypePrefix), + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: "sourcegraph-frontend", + Port: netv1.ServiceBackendPort{ + Number: 30081, + }, + }, + }, + }}, + }, + }, + }} + ingressClassName := "nginx" + testIngress.Spec.IngressClassName = &ingressClassName + _, err = suite.k8sClient.NetworkingV1().Ingresses(namespace.Name).Create(suite.ctx, &testIngress, metav1.CreateOptions{}) + suite.Require().NoError(err) + + cfgMap := suite.newConfigMap(namespace.GetName(), "frontend/with-ingress") + suite.awaitReconciliation(namespace.GetName(), func() { + _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace.GetName()).Create(suite.ctx, cfgMap, metav1.CreateOptions{}) + suite.Require().NoError(err) + }) + suite.makeGoldenAssertions(namespace.GetName(), "frontend/adopt-service") +} + +func (suite *ApplianceTestSuite) TestFrontendDeploymentRollsWhenPGSecretsChange() { + for _, tc := range []struct { + secret string + }{ + {secret: pgsqlSecretName}, + {secret: codeInsightsDBSecretName}, + {secret: codeIntelDBSecretName}, + } { + suite.Run(tc.secret, func() { + // Create the frontend before the PGSQL secret exists. In general, this + // might happen, depending on the order of the reconcile loop. If we + // introducce concurrency to this, we'll have little control over what + // happens first. + namespace := suite.createConfigMapAndAwaitReconciliation("frontend/default") + + // Create the PGSQL secret. + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.secret, + }, + StringData: map[string]string{ + "host": "example.com", + "port": "5432", + "user": "alice", + "password": "letmein", + "database": "sg", + }, + } + _, err := suite.k8sClient.CoreV1().Secrets(namespace).Create(suite.ctx, secret, metav1.CreateOptions{}) + suite.Require().NoError(err) + + // We have to make a config change to trigger the reconcile loop + suite.awaitReconciliation(namespace, func() { + cfgMap := suite.newConfigMap(namespace, "frontend/default") + cfgMap.GetAnnotations()["force-reconcile"] = "1" + _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace).Update(suite.ctx, cfgMap, metav1.UpdateOptions{}) + suite.Require().NoError(err) + }) + + suite.makeGoldenAssertions(namespace, fmt.Sprintf("frontend/after-create-%s-secret", tc.secret)) + }) + } +} + +func (suite *ApplianceTestSuite) TestFrontendDeploymentRollsWhenRedisSecretsChange() { + for _, tc := range []struct { + secret string + }{ + {secret: redisCacheSecretName}, + {secret: redisStoreSecretName}, + } { + suite.Run(tc.secret, func() { + // Create the frontend before the PGSQL secret exists. In general, this + // might happen, depending on the order of the reconcile loop. If we + // introducce concurrency to this, we'll have little control over what + // happens first. + namespace := suite.createConfigMapAndAwaitReconciliation("frontend/default") + + // Create the PGSQL secret. + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.secret, + }, + StringData: map[string]string{ + "endpoint": "example.com", + }, + } + _, err := suite.k8sClient.CoreV1().Secrets(namespace).Create(suite.ctx, secret, metav1.CreateOptions{}) + suite.Require().NoError(err) + + // We have to make a config change to trigger the reconcile loop + suite.awaitReconciliation(namespace, func() { + cfgMap := suite.newConfigMap(namespace, "frontend/default") + cfgMap.GetAnnotations()["force-reconcile"] = "1" + _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace).Update(suite.ctx, cfgMap, metav1.UpdateOptions{}) + suite.Require().NoError(err) + }) + + suite.makeGoldenAssertions(namespace, fmt.Sprintf("frontend/after-create-%s-secret", tc.secret)) + }) + } +} + +type MockObject struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Data map[string]string `json:"data,omitempty"` +} + +func (m *MockObject) DeepCopyObject() runtime.Object { + if m == nil { + return nil + } + return &MockObject{ + TypeMeta: m.TypeMeta, + ObjectMeta: *m.ObjectMeta.DeepCopy(), + Data: maps.Clone(m.Data), + } +} + +func (suite *ApplianceTestSuite) TestMergeK8sObjects() { + tests := []struct { + name string + existingObj client.Object + newObject client.Object + expected client.Object + expectError bool + }{ + { + name: "Successful merge", + existingObj: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + }, + }, + newObject: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key2": "value2", + }, + }, + expected: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + expectError: false, + }, + { + name: "Merge with overlapping keys", + existingObj: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "old-value2", + }, + }, + newObject: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key2": "new-value2", + "key3": "value3", + }, + }, + expected: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "new-value2", + "key3": "value3", + }, + }, + expectError: false, + }, + { + name: "Merge with empty new object", + existingObj: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + }, + }, + newObject: &MockObject{}, + expected: &MockObject{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-config", + }, + Data: map[string]string{ + "key1": "value1", + }, + }, + expectError: false, + }, + { + name: "merges annotations", + existingObj: &MockObject{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "present_and_unchanged": "old1", + "present_and_changed": "old2", + }, + }, + }, + newObject: &MockObject{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "present_and_changed": "new2", + "new": "new3", + }, + }, + }, + expected: &MockObject{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "present_and_unchanged": "old1", + "present_and_changed": "new2", + "new": "new3", + }, + }, + }, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result, err := MergeK8sObjects(tt.existingObj, tt.newObject) + fmt.Print(result) + if tt.expectError { + assert.Error(suite.T(), err) + assert.Nil(suite.T(), result) + } else { + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), tt.expected, result) + } + }) + } +} diff --git a/internal/appliance/reconciler/gitserver.go b/internal/appliance/reconciler/gitserver.go index c0328802264d0..5caad0ecb71f7 100644 --- a/internal/appliance/reconciler/gitserver.go +++ b/internal/appliance/reconciler/gitserver.go @@ -92,6 +92,16 @@ func (r *Reconciler) reconcileGitServerStatefulSet(ctx context.Context, sg *conf } podTemplate := pod.NewPodTemplate(name, cfg) + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + podTemplate.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash + podTemplate.Template.Spec.Containers = []corev1.Container{ctr} podTemplate.Template.Spec.ServiceAccountName = name podTemplate.Template.Spec.Volumes = podVolumes @@ -105,7 +115,14 @@ func (r *Reconciler) reconcileGitServerStatefulSet(ctx context.Context, sg *conf sset.Spec.Template = podTemplate.Template sset.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{pvc} - return reconcileObject(ctx, r, sg.Spec.GitServer, &sset, &appsv1.StatefulSet{}, sg, owner) + ifChanged := struct { + config.GitServerSpec + RedisConnSpecs + }{ + GitServerSpec: cfg, + RedisConnSpecs: redisConnSpecs, + } + return reconcileObject(ctx, r, ifChanged, &sset, &appsv1.StatefulSet{}, sg, owner) } func (r *Reconciler) reconcileGitServerService(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { diff --git a/internal/appliance/reconciler/helpers_test.go b/internal/appliance/reconciler/helpers_test.go index ff35c692b640f..24f00878113db 100644 --- a/internal/appliance/reconciler/helpers_test.go +++ b/internal/appliance/reconciler/helpers_test.go @@ -2,8 +2,6 @@ package reconciler import ( "context" - "crypto/rand" - "encoding/hex" "os" "path/filepath" "testing" @@ -63,21 +61,17 @@ func (suite *ApplianceTestSuite) TearDownSuite() { func (suite *ApplianceTestSuite) createConfigMapAndAwaitReconciliation(fixtureFileName string) string { // Create a random namespace for each test - namespace := "test-appliance-" + suite.randomSlug() - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - _, err := suite.k8sClient.CoreV1().Namespaces().Create(suite.ctx, ns, metav1.CreateOptions{}) + namespace, err := k8senvtest.NewRandomNamespace("test-appliance") + suite.Require().NoError(err) + _, err = suite.k8sClient.CoreV1().Namespaces().Create(suite.ctx, namespace, metav1.CreateOptions{}) suite.Require().NoError(err) - cfgMap := suite.newConfigMap(namespace, fixtureFileName) - suite.awaitReconciliation(namespace, func() { - _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace).Create(suite.ctx, cfgMap, metav1.CreateOptions{}) + cfgMap := suite.newConfigMap(namespace.GetName(), fixtureFileName) + suite.awaitReconciliation(namespace.GetName(), func() { + _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace.GetName()).Create(suite.ctx, cfgMap, metav1.CreateOptions{}) suite.Require().NoError(err) }) - return namespace + return namespace.GetName() } func (suite *ApplianceTestSuite) updateConfigMapAndAwaitReconciliation(namespace, fixtureFileName string) { @@ -131,10 +125,3 @@ func (suite *ApplianceTestSuite) newConfigMap(namespace, fixtureFileName string) Data: map[string]string{"spec": string(cfgBytes)}, } } - -func (suite *ApplianceTestSuite) randomSlug() string { - buf := make([]byte, 3) - _, err := rand.Read(buf) - suite.Require().NoError(err) - return hex.EncodeToString(buf) -} diff --git a/internal/appliance/reconciler/indexed_search.go b/internal/appliance/reconciler/indexed_search.go index 358afd34ce6e2..b2030a083c32b 100644 --- a/internal/appliance/reconciler/indexed_search.go +++ b/internal/appliance/reconciler/indexed_search.go @@ -128,7 +128,7 @@ func (r *Reconciler) reconcileIndexedSearchIndexerService(ctx context.Context, s "prometheus.io/port": "6072", "sourcegraph.prometheus/scrape": "true", }) - svc.Spec.Ports = []corev1.ServicePort{{Port: 6072, TargetPort: intstr.FromInt(6072)}} + svc.Spec.Ports = []corev1.ServicePort{{Port: 6072, TargetPort: intstr.FromInt32(6072)}} svc.Spec.Selector = map[string]string{ "app": "indexed-search", } diff --git a/internal/appliance/reconciler/kubernetes.go b/internal/appliance/reconciler/kubernetes.go index d570e9a1b0123..7f87fc8074442 100644 --- a/internal/appliance/reconciler/kubernetes.go +++ b/internal/appliance/reconciler/kubernetes.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/log" "github.com/sourcegraph/sourcegraph/internal/appliance/config" @@ -28,7 +29,7 @@ func reconcileObject[T client.Object]( sg *config.Sourcegraph, owner client.Object, ) error { if cfg.IsDisabled() { - return r.ensureObjectDeleted(ctx, obj) + return ensureObjectDeleted(ctx, r, owner, obj) } updateIfChanged := struct { @@ -62,7 +63,11 @@ func createOrUpdateObject[R client.Object]( ctx context.Context, r *Reconciler, updateIfChanged any, owner client.Object, obj, objKind R, ) error { - logger := log.FromContext(ctx).WithValues("kind", obj.GetObjectKind().GroupVersionKind(), "namespace", obj.GetNamespace(), "name", obj.GetName()) + gvk, err := apiutil.GVKForObject(obj, r.Scheme) + if err != nil { + return errors.Wrap(err, "getting GVK for object") + } + logger := log.FromContext(ctx).WithValues("kind", gvk.String(), "namespace", obj.GetNamespace(), "name", obj.GetName()) namespacedName := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()} cfgHash, err := configHash(updateIfChanged) @@ -81,7 +86,7 @@ func createOrUpdateObject[R client.Object]( // error: "cluster-scoped resource must not have a namespace-scoped owner". // non-namespaced resources will therefore not be garbage-collected when the // ConfigMap is deleted. - if !isNamespaced(obj) { + if isNamespaced(obj) { if err := ctrl.SetControllerReference(owner, obj, r.Scheme); err != nil { return errors.Newf("setting controller reference: %w", err) } @@ -102,6 +107,11 @@ func createOrUpdateObject[R client.Object]( return err } + if !isControlledBy(owner, existingRes) && isNamespaced(obj) && !config.ShouldAdopt(obj) { + logger.Info("refusing to update non-owned resource") + return nil + } + if cfgHash != existingRes.GetAnnotations()[config.AnnotationKeyConfigHash] { logger.Info("Found existing object with spec that does not match desired state. Clobbering it.") if err := r.Client.Update(ctx, obj); err != nil { @@ -117,18 +127,40 @@ func createOrUpdateObject[R client.Object]( func isNamespaced(obj client.Object) bool { if _, ok := obj.(*rbacv1.ClusterRole); ok { - return true + return false } if _, ok := obj.(*rbacv1.ClusterRoleBinding); ok { - return true + return false } - return false + return true } -func (r *Reconciler) ensureObjectDeleted(ctx context.Context, obj client.Object) error { - logger := log.FromContext(ctx).WithValues("kind", obj.GetObjectKind().GroupVersionKind(), "namespace", obj.GetNamespace(), "name", obj.GetName()) +func ensureObjectDeleted[T client.Object](ctx context.Context, r *Reconciler, owner client.Object, obj T) error { + // We need to try to get the object first, in order to check its owner + // references later. + objKey := types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()} + if err := r.Client.Get(ctx, objKey, obj); err != nil { + if kerrors.IsNotFound(err) { + // Object doesn't exist, we don't need to delete it + return nil + } + } + gvk, err := apiutil.GVKForObject(obj, r.Scheme) + if err != nil { + return errors.Wrap(err, "getting GVK for object") + } + + logger := log.FromContext(ctx).WithValues("kind", gvk.String(), "namespace", obj.GetNamespace(), "name", obj.GetName()) + + if !isControlledBy(owner, obj) && isNamespaced(obj) { + logger.Info("refusing to delete non-owned resource") + return nil + } + + logger.Info("deleting resource") if err := r.Client.Delete(ctx, obj); err != nil { if kerrors.IsNotFound(err) { + // If by chance it got deleted concurrently, no harm done. return nil } @@ -138,6 +170,15 @@ func (r *Reconciler) ensureObjectDeleted(ctx context.Context, obj client.Object) return nil } +func isControlledBy(owner, obj client.Object) bool { + for _, ownerRef := range obj.GetOwnerReferences() { + if owner.GetUID() == ownerRef.UID { + return true + } + } + return false +} + func configHash(configElement any) (string, error) { cfgBytes, err := json.Marshal(configElement) if err != nil { diff --git a/internal/appliance/reconciler/otel_agent.go b/internal/appliance/reconciler/otel_agent.go new file mode 100644 index 0000000000000..cb64e5c1932fc --- /dev/null +++ b/internal/appliance/reconciler/otel_agent.go @@ -0,0 +1,102 @@ +package reconciler + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/sourcegraph/internal/appliance/config" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/configmap" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/container" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/daemonset" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/pod" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/serviceaccount" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// TODO split agent from collector? Some StandardConfig features might not work +// particularly well if not... e.g. SA annotations +func (r *Reconciler) reconcileOtel(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { + if err := r.reconcileOtelAgentConfigmap(ctx, sg, owner); err != nil { + return errors.Wrap(err, "reconciling ConfigMap") + } + if err := r.reconcileOtelAgentServiceAccount(ctx, sg, owner); err != nil { + return errors.Wrap(err, "reconciling ServiceAccount") + } + if err := r.reconcileOtelAgentDaemonset(ctx, sg, owner); err != nil { + return errors.Wrap(err, "reconciling DaemonSet") + } + return nil +} + +func (r *Reconciler) reconcileOtelAgentConfigmap(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { + name := "otel-agent" + cfg := sg.Spec.OtelAgent + cm := configmap.NewConfigMap(name, sg.Namespace) + cm.Data = map[string]string{ + "config.yaml": string(config.OtelAgentConfig), + } + return reconcileObject(ctx, r, cfg, &cm, &corev1.ConfigMap{}, sg, owner) +} + +func (r *Reconciler) reconcileOtelAgentServiceAccount(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { + cfg := sg.Spec.OtelAgent + sa := serviceaccount.NewServiceAccount("otel-agent", sg.Namespace, cfg) + return reconcileObject(ctx, r, cfg, &sa, &corev1.ServiceAccount{}, sg, owner) +} + +func (r *Reconciler) reconcileOtelAgentDaemonset(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { + name := "otel-agent" + cfg := sg.Spec.OtelAgent + + ctr := container.NewContainer(name, cfg, config.ContainerConfig{ + Image: config.GetDefaultImage(sg, "opentelemetry-collector"), + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + }, + }) + ctr.Command = []string{"/bin/otelcol-sourcegraph", "--config=/etc/otel-agent/config.yaml"} + + probe := &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/", Port: intstr.FromInt(13133)}, + }, + } + ctr.ReadinessProbe = probe + ctr.LivenessProbe = probe + + ctr.Ports = []corev1.ContainerPort{ + {Name: "zpages", ContainerPort: 55679, HostPort: 55679}, + {Name: "otel-grpc", ContainerPort: 4317, HostPort: 4317}, + {Name: "otel-http", ContainerPort: 4318, HostPort: 4318}, + } + + ctr.VolumeMounts = []corev1.VolumeMount{ + {Name: "config", MountPath: "/etc/otel-agent"}, + } + + template := pod.NewPodTemplate(name, cfg) + template.Template.Spec.Containers = []corev1.Container{ctr} + + cfgVol := pod.NewVolumeFromConfigMap("config", name) + cfgVol.VolumeSource.ConfigMap.Items = []corev1.KeyToPath{ + {Key: "config.yaml", Path: "config.yaml"}, + } + template.Template.Spec.Volumes = []corev1.Volume{cfgVol} + + ds := daemonset.New(name, sg.Namespace, sg.Spec.RequestedVersion) + ds.Spec.Template = template.Template + + return reconcileObject(ctx, r, cfg, &ds, &appsv1.DaemonSet{}, sg, owner) +} diff --git a/internal/appliance/reconciler/otel_agent_test.go b/internal/appliance/reconciler/otel_agent_test.go new file mode 100644 index 0000000000000..0ad92a974855b --- /dev/null +++ b/internal/appliance/reconciler/otel_agent_test.go @@ -0,0 +1,14 @@ +package reconciler + +func (suite *ApplianceTestSuite) TestDeployOtelAgent() { + for _, tc := range []struct { + name string + }{ + {name: "otel-agent/default"}, + } { + suite.Run(tc.name, func() { + namespace := suite.createConfigMapAndAwaitReconciliation(tc.name) + suite.makeGoldenAssertions(namespace, tc.name) + }) + } +} diff --git a/internal/appliance/reconciler/precise_code_intel.go b/internal/appliance/reconciler/precise_code_intel.go index 38ee16a7a2334..c499c90d1f629 100644 --- a/internal/appliance/reconciler/precise_code_intel.go +++ b/internal/appliance/reconciler/precise_code_intel.go @@ -104,8 +104,8 @@ func (r *Reconciler) reconcilePreciseCodeIntelDeployment(ctx context.Context, sg dep := deployment.NewDeployment(name, sg.Namespace, sg.Spec.RequestedVersion) dep.Spec.Replicas = pointers.Ptr(cfg.Replicas) dep.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ - MaxSurge: pointers.Ptr(intstr.FromInt(1)), - MaxUnavailable: pointers.Ptr(intstr.FromInt(1)), + MaxSurge: pointers.Ptr(intstr.FromInt32(1)), + MaxUnavailable: pointers.Ptr(intstr.FromInt32(1)), } dep.Spec.Template = podTemplate.Template diff --git a/internal/appliance/reconciler/reconcile.go b/internal/appliance/reconciler/reconcile.go index 4aa034c9cdd2c..9dd9a58a31e83 100644 --- a/internal/appliance/reconciler/reconcile.go +++ b/internal/appliance/reconciler/reconcile.go @@ -2,6 +2,7 @@ package reconciler import ( "context" + "sync" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/types" @@ -24,12 +25,17 @@ import ( var _ reconcile.Reconciler = &Reconciler{} type Reconciler struct { + sync.Mutex client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder + Scheme *runtime.Scheme + Recorder record.EventRecorder + BeginHealthCheckLoop chan struct{} } func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Mutex.Lock() + defer r.Mutex.Unlock() + reqLog := log.FromContext(ctx) reqLog.Info("reconciling sourcegraph appliance") @@ -50,6 +56,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // tests, if it isn't useful elsewhere. defer r.Recorder.Event(&applianceSpec, "Normal", "ReconcileFinished", "Reconcile finished.") + status := applianceSpec.GetAnnotations()[config.AnnotationKeyStatus] + if r.BeginHealthCheckLoop != nil && config.IsPostInstallStatus(config.Status(status)) { + close(r.BeginHealthCheckLoop) + r.BeginHealthCheckLoop = nil + } + // TODO place holder code until we get the configmap spec'd out and working' data, ok := applianceSpec.Data["spec"] if !ok { @@ -127,6 +139,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err := r.reconcileJaeger(ctx, &sourcegraph, &applianceSpec); err != nil { return ctrl.Result{}, errors.Newf("failed to reconcile jaeger: %w", err) } + if err := r.reconcileOtel(ctx, &sourcegraph, &applianceSpec); err != nil { + return ctrl.Result{}, errors.Newf("failed to reconcile OpenTelemetry Collector: %w", err) + } // Set the current version annotation in case migration logic depends on it. applianceSpec.Annotations[config.AnnotationKeyCurrentVersion] = sourcegraph.Spec.RequestedVersion diff --git a/internal/appliance/reconciler/repo_updater.go b/internal/appliance/reconciler/repo_updater.go index 45354967354fd..bc4b5c90796d1 100644 --- a/internal/appliance/reconciler/repo_updater.go +++ b/internal/appliance/reconciler/repo_updater.go @@ -95,13 +95,30 @@ func (r *Reconciler) reconcileRepoUpdaterDeployment(ctx context.Context, sg *con } podTemplate := pod.NewPodTemplate(name, cfg) + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + podTemplate.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash + podTemplate.Template.Spec.Containers = []corev1.Container{ctr} dep := deployment.NewDeployment(name, sg.Namespace, sg.Spec.RequestedVersion) dep.Spec.Template = podTemplate.Template dep.Spec.Template.Spec.ServiceAccountName = name - return reconcileObject(ctx, r, sg.Spec.RepoUpdater, &dep, &appsv1.Deployment{}, sg, owner) + ifChanged := struct { + config.RepoUpdaterSpec + RedisConnSpecs + }{ + RepoUpdaterSpec: cfg, + RedisConnSpecs: redisConnSpecs, + } + return reconcileObject(ctx, r, ifChanged, &dep, &appsv1.Deployment{}, sg, owner) } func (r *Reconciler) reconcileRepoUpdaterServiceAccount(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { diff --git a/internal/appliance/reconciler/searcher.go b/internal/appliance/reconciler/searcher.go index a40149526744c..c842b14f38875 100644 --- a/internal/appliance/reconciler/searcher.go +++ b/internal/appliance/reconciler/searcher.go @@ -91,6 +91,16 @@ func (r *Reconciler) reconcileSearcherStatefulSet(ctx context.Context, sg *confi } podTemplate := pod.NewPodTemplate(name, cfg) + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + podTemplate.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash + podTemplate.Template.Spec.Containers = []corev1.Container{ctr} podTemplate.Template.Spec.Volumes = []corev1.Volume{ {Name: "cache"}, @@ -108,7 +118,14 @@ func (r *Reconciler) reconcileSearcherStatefulSet(ctx context.Context, sg *confi sset.Spec.Replicas = &cfg.Replicas sset.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{pvc} - return reconcileObject(ctx, r, cfg, &sset, &appsv1.StatefulSet{}, sg, owner) + ifChanged := struct { + config.SearcherSpec + RedisConnSpecs + }{ + SearcherSpec: cfg, + RedisConnSpecs: redisConnSpecs, + } + return reconcileObject(ctx, r, ifChanged, &sset, &appsv1.StatefulSet{}, sg, owner) } func (r *Reconciler) reconcileSearcherService(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { diff --git a/internal/appliance/reconciler/secret_management.go b/internal/appliance/reconciler/secret_management.go new file mode 100644 index 0000000000000..a77661d223190 --- /dev/null +++ b/internal/appliance/reconciler/secret_management.go @@ -0,0 +1,122 @@ +package reconciler + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/sourcegraph/sourcegraph/internal/appliance/config" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Utilities to cause rolling deployments when secrets change live here. +// Indirectly tested through service-definition-specific golden tests. + +const ( + pgsqlSecretName = "pgsql-auth" + codeInsightsDBSecretName = "codeinsights-db-auth" + codeIntelDBSecretName = "codeintel-db-auth" + redisCacheSecretName = "redis-cache" + redisStoreSecretName = "redis-store" +) + +type DBConnSpecs struct { + PG *config.DatabaseConnectionSpec `json:"pg,omitempty"` + CodeIntel *config.DatabaseConnectionSpec `json:"codeintel,omitempty"` + CodeInsights *config.DatabaseConnectionSpec `json:"codeinsights,omitempty"` +} + +type RedisConnSpecs struct { + Cache string `json:"cache,omitempty"` + Store string `json:"store,omitempty"` +} + +func (r *Reconciler) getDBSecrets(ctx context.Context, sg *config.Sourcegraph) (DBConnSpecs, error) { + dbConnSpec, err := r.getDBSecret(ctx, sg, pgsqlSecretName) + if err != nil { + return DBConnSpecs{}, err + } + codeIntelConnSpec, err := r.getDBSecret(ctx, sg, codeIntelDBSecretName) + if err != nil { + return DBConnSpecs{}, err + } + codeInsightsConnSpec, err := r.getDBSecret(ctx, sg, codeInsightsDBSecretName) + if err != nil { + return DBConnSpecs{}, err + } + return DBConnSpecs{ + PG: dbConnSpec, + CodeIntel: codeIntelConnSpec, + CodeInsights: codeInsightsConnSpec, + }, nil +} + +func (r *Reconciler) getRedisSecrets(ctx context.Context, sg *config.Sourcegraph) (RedisConnSpecs, error) { + redisCacheEndpoint, err := r.getRedisSecret(ctx, sg, redisCacheSecretName) + if err != nil { + return RedisConnSpecs{}, err + } + redisStoreEndpoint, err := r.getRedisSecret(ctx, sg, redisStoreSecretName) + if err != nil { + return RedisConnSpecs{}, err + } + return RedisConnSpecs{ + Cache: redisCacheEndpoint, + Store: redisStoreEndpoint, + }, nil +} + +func (r *Reconciler) getDBSecret(ctx context.Context, sg *config.Sourcegraph, secretName string) (*config.DatabaseConnectionSpec, error) { + dbSecret, err := r.getSecret(ctx, sg, secretName) + if err != nil { + return nil, err + } + + return &config.DatabaseConnectionSpec{ + Host: string(dbSecret.Data["host"]), + Port: string(dbSecret.Data["port"]), + User: string(dbSecret.Data["user"]), + Password: string(dbSecret.Data["password"]), + Database: string(dbSecret.Data["database"]), + }, nil +} + +func (r *Reconciler) getRedisSecret(ctx context.Context, sg *config.Sourcegraph, secretName string) (string, error) { + redisSecret, err := r.getSecret(ctx, sg, secretName) + if err != nil { + return "", err + } + + return string(redisSecret.Data["endpoint"]), nil +} + +func (r *Reconciler) getSecret(ctx context.Context, sg *config.Sourcegraph, secretName string) (*corev1.Secret, error) { + var secret corev1.Secret + secretNsName := types.NamespacedName{Name: secretName, Namespace: sg.Namespace} + if err := r.Client.Get(ctx, secretNsName, &secret); err != nil { + if !kerrors.IsNotFound(err) { + return nil, errors.Wrapf(err, "getting secret %s", secretName) + } + + // If we cannot find the secret, return nil but also no error. We can + // still serialize an ifChanged object in reconcileFrontendDeployment(). + // We should do this rather than fail the reconcile loop here, because + // Kubernetes does not have inter-service dependencies, so it is + // idiomatic to finish the loop even if the desired global final state + // has not been reached. The next reconciliation after the secret exists + // will yield a different result, which will cause deployed pods to roll + // (since the spec.template.metadata.annotations changes). + // + // We return a zero-valued secret to avoid nil pointer explosions. All + // data fields will be empty. Currently, all callers only use this + // function to hash the data to see if its changed, so this seems ok to + // do. + log.FromContext(ctx).Info("could not find secret", "secretName", secretName, "err", err) + return &corev1.Secret{}, nil + } + + return &secret, nil +} diff --git a/internal/appliance/reconciler/standard_config_test.go b/internal/appliance/reconciler/standard_config_test.go index a58f3b64c3032..a8a91b8a5e243 100644 --- a/internal/appliance/reconciler/standard_config_test.go +++ b/internal/appliance/reconciler/standard_config_test.go @@ -1,5 +1,12 @@ package reconciler +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest" +) + // Use this file to test features available in StandardConfig (see // development.md and config subpackage). @@ -33,3 +40,39 @@ func (suite *ApplianceTestSuite) TestResourcesDeletedWhenDisabled() { suite.updateConfigMapAndAwaitReconciliation(namespace, "standard/everything-disabled") suite.makeGoldenAssertions(namespace, "standard/blobstore-subsequent-disable") } + +func (suite *ApplianceTestSuite) TestDoesNotDeleteUnownedResources() { + namespace, err := k8senvtest.NewRandomNamespace("test-appliance") + suite.Require().NoError(err) + _, err = suite.k8sClient.CoreV1().Namespaces().Create(suite.ctx, namespace, metav1.CreateOptions{}) + suite.Require().NoError(err) + + // Example: the admin configures a pgsql secret that references an external + // database, and therefore disables pgsql in appliance config. + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pgsqlSecretName, + }, + StringData: map[string]string{ + "host": "example.com", + "port": "5432", + "user": "alice", + "password": "letmein", + "database": "sg", + }, + } + _, err = suite.k8sClient.CoreV1().Secrets(namespace.Name).Create(suite.ctx, secret, metav1.CreateOptions{}) + suite.Require().NoError(err) + + suite.awaitReconciliation(namespace.Name, func() { + // This is an artificial test fixture that disables everything except + // frontend, but this is representative of disabling pgsql. + cfgMap := suite.newConfigMap(namespace.Name, "frontend/default") + _, err := suite.k8sClient.CoreV1().ConfigMaps(namespace.GetName()).Create(suite.ctx, cfgMap, metav1.CreateOptions{}) + suite.Require().NoError(err) + }) + + secretStillPresent, err := suite.k8sClient.CoreV1().Secrets(namespace.Name).Get(suite.ctx, pgsqlSecretName, metav1.GetOptions{}) + suite.Require().NoError(err) + suite.Require().Equal("example.com", string(secretStillPresent.Data["host"])) +} diff --git a/internal/appliance/reconciler/symbols.go b/internal/appliance/reconciler/symbols.go index 9f934fc77143b..d1e8bca681a07 100644 --- a/internal/appliance/reconciler/symbols.go +++ b/internal/appliance/reconciler/symbols.go @@ -106,6 +106,16 @@ func (r *Reconciler) reconcileSymbolsStatefulSet(ctx context.Context, sg *config } podTemplate := pod.NewPodTemplate(name, cfg) + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + podTemplate.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash + podTemplate.Template.Spec.Containers = []corev1.Container{ctr} podTemplate.Template.Spec.ServiceAccountName = name podTemplate.Template.Spec.Volumes = []corev1.Volume{ @@ -122,7 +132,14 @@ func (r *Reconciler) reconcileSymbolsStatefulSet(ctx context.Context, sg *config sset.Spec.Template = podTemplate.Template sset.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{pvc} - return reconcileObject(ctx, r, sg.Spec.Symbols, &sset, &appsv1.StatefulSet{}, sg, owner) + ifChanged := struct { + config.SymbolsSpec + RedisConnSpecs + }{ + SymbolsSpec: cfg, + RedisConnSpecs: redisConnSpecs, + } + return reconcileObject(ctx, r, ifChanged, &sset, &appsv1.StatefulSet{}, sg, owner) } func (r *Reconciler) reconcileSymbolsService(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { diff --git a/internal/appliance/reconciler/syntect.go b/internal/appliance/reconciler/syntect.go index 77ab2bbdb30ce..d0ecc45ac7ab3 100644 --- a/internal/appliance/reconciler/syntect.go +++ b/internal/appliance/reconciler/syntect.go @@ -78,8 +78,8 @@ func (r *Reconciler) reconcileSyntectDeployment(ctx context.Context, sg *config. dep := deployment.NewDeployment(name, sg.Namespace, sg.Spec.RequestedVersion) dep.Spec.Replicas = pointers.Ptr(cfg.Replicas) dep.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ - MaxSurge: pointers.Ptr(intstr.FromInt(1)), - MaxUnavailable: pointers.Ptr(intstr.FromInt(0)), + MaxSurge: pointers.Ptr(intstr.FromInt32(1)), + MaxUnavailable: pointers.Ptr(intstr.FromInt32(0)), } dep.Spec.Template = podTemplate.Template diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/blobstore/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/blobstore/default.yaml index 4085736221cff..26ece4a0215f6 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/blobstore/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/blobstore/default.yaml @@ -114,7 +114,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/cadvisor/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/cadvisor/default.yaml index e581d7bf76e6c..c2944f5f2197b 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/cadvisor/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/cadvisor/default.yaml @@ -151,7 +151,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/codeinsights/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/codeinsights/default.yaml index 6160a8d29df02..21dd32796856a 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/codeinsights/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/codeinsights/default.yaml @@ -303,7 +303,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/codeintel/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/codeintel/default.yaml index 518313d30806c..813b4028ea643 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/codeintel/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/codeintel/default.yaml @@ -323,7 +323,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/adopt-service.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/adopt-service.yaml new file mode 100644 index 0000000000000..2f4bda5569ed9 --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/adopt-service.yaml @@ -0,0 +1,586 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: eca9597d0eccefd3eb40039ed5d5afaf1b81989733f82692b933206540242d93 + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 1 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: + ingress: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 1 + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + ingressClassName: nginx + rules: + - host: an-existing-hostname.com + http: + paths: + - backend: + service: + name: sourcegraph-frontend + port: + number: 30081 + path: / + pathType: Prefix + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeinsights-db-auth-secret.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeinsights-db-auth-secret.yaml new file mode 100644 index 0000000000000..2c44621cf7f2f --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeinsights-db-auth-secret.yaml @@ -0,0 +1,567 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: 8ba33f0d5cd3a002002cb645199a3566f6848bb71563df8d3fc5ba0d44f4b2ba + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 2 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: 34304350e7a9487e6da52204b791f199519537b3e12598c3f81427228002f026 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + force-reconcile: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + database: c2c= + host: ZXhhbXBsZS5jb20= + password: bGV0bWVpbg== + port: NTQzMg== + user: YWxpY2U= + kind: Secret + metadata: + creationTimestamp: "2024-04-19T00:00:00Z" + name: codeinsights-db-auth + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + type: Opaque + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + app.kubernetes.io/component: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeintel-db-auth-secret.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeintel-db-auth-secret.yaml new file mode 100644 index 0000000000000..16192df74b1ed --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-codeintel-db-auth-secret.yaml @@ -0,0 +1,567 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: 9749a17bd6b4982408effad39834fd1b74760e0518af90e920e9a418904cb792 + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 2 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: 42a16845bd61ad6619bcd81416e81c6e1761497b10b0c3082b04e459b8f75364 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + force-reconcile: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + database: c2c= + host: ZXhhbXBsZS5jb20= + password: bGV0bWVpbg== + port: NTQzMg== + user: YWxpY2U= + kind: Secret + metadata: + creationTimestamp: "2024-04-19T00:00:00Z" + name: codeintel-db-auth + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + type: Opaque + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + app.kubernetes.io/component: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-pgsql-auth-secret.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-pgsql-auth-secret.yaml new file mode 100644 index 0000000000000..2ccfcce6db464 --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-pgsql-auth-secret.yaml @@ -0,0 +1,567 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: 95c83c46bf588f45d3a2cc4b56b5d16ee8469f734f967ab0080702f7f8a11a9d + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 2 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: a9aa88b6e9c7d8a774279a6a7afe96c66e6522533ca99f1b1eb7e80bb40bc3ff + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + force-reconcile: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + database: c2c= + host: ZXhhbXBsZS5jb20= + password: bGV0bWVpbg== + port: NTQzMg== + user: YWxpY2U= + kind: Secret + metadata: + creationTimestamp: "2024-04-19T00:00:00Z" + name: pgsql-auth + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + type: Opaque + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + app.kubernetes.io/component: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-cache-secret.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-cache-secret.yaml new file mode 100644 index 0000000000000..a60f2073f8a0b --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-cache-secret.yaml @@ -0,0 +1,563 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: 830582994b55681116d55839f588157f1cfa9f35d2f3144c0dbfdebaa20e6cb5 + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 2 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: e548b6ad5e1acfcdc6b6071ca1ec718a97484e185109d353a2a9903c1820e8d1 + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + force-reconcile: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + endpoint: ZXhhbXBsZS5jb20= + kind: Secret + metadata: + creationTimestamp: "2024-04-19T00:00:00Z" + name: redis-cache + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + type: Opaque + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + app.kubernetes.io/component: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-store-secret.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-store-secret.yaml new file mode 100644 index 0000000000000..7552f2c9d6a47 --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/after-create-redis-store-secret.yaml @@ -0,0 +1,563 @@ +resources: + - apiVersion: apps/v1 + kind: Deployment + metadata: + annotations: + appliance.sourcegraph.com/configHash: a05c531887e44fa312cc6727d257e59a8454d8355d4fad78c6d699d20cad9821 + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 2 + labels: + app.kubernetes.io/component: sourcegraph-frontend + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + progressDeadlineSeconds: 600 + replicas: 2 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: sourcegraph-frontend + strategy: + rollingUpdate: + maxSurge: 2 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: b704bb5cf2d3a4cc9bfd6893c54ea5e941031731458500d509acfc5f894afd3e + kubectl.kubernetes.io/default-container: sourcegraph-frontend + creationTimestamp: null + labels: + app: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + spec: + containers: + - args: + - serve + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + - name: REDIS_CACHE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-cache + - name: REDIS_STORE_ENDPOINT + valueFrom: + secretKeyRef: + key: endpoint + name: redis-store + - name: OTEL_AGENT_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://$(OTEL_AGENT_HOST):4317 + image: index.docker.io/sourcegraph/frontend:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: debug + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + name: frontend + ports: + - containerPort: 3080 + name: http + protocol: TCP + - containerPort: 3090 + name: http-internal + protocol: TCP + - containerPort: 6060 + name: debug + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /ready + port: debug + scheme: HTTP + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: "2" + ephemeral-storage: 8Gi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 4Gi + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /home/sourcegraph + name: home-dir + dnsPolicy: ClusterFirst + initContainers: + - args: + - up + env: + - name: DEPLOY_TYPE + value: appliance + - name: PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: pgsql-auth + - name: PGHOST + valueFrom: + secretKeyRef: + key: host + name: pgsql-auth + - name: PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: pgsql-auth + - name: PGPORT + valueFrom: + secretKeyRef: + key: port + name: pgsql-auth + - name: PGUSER + valueFrom: + secretKeyRef: + key: user + name: pgsql-auth + - name: CODEINTEL_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeintel-db-auth + - name: CODEINTEL_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeintel-db-auth + - name: CODEINTEL_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeintel-db-auth + - name: CODEINTEL_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeintel-db-auth + - name: CODEINTEL_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeintel-db-auth + - name: CODEINSIGHTS_PGDATABASE + valueFrom: + secretKeyRef: + key: database + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGHOST + valueFrom: + secretKeyRef: + key: host + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPASSWORD + valueFrom: + secretKeyRef: + key: password + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGPORT + valueFrom: + secretKeyRef: + key: port + name: codeinsights-db-auth + - name: CODEINSIGHTS_PGUSER + valueFrom: + secretKeyRef: + key: user + name: codeinsights-db-auth + image: index.docker.io/sourcegraph/migrator:5.3.9104 + imagePullPolicy: IfNotPresent + name: migrator + resources: + limits: + cpu: 500m + memory: 100M + requests: + cpu: 100m + memory: 50M + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + serviceAccount: sourcegraph-frontend + serviceAccountName: sourcegraph-frontend + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: home-dir + status: {} + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: {} + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryCollector: + disabled: true + + openTelemetryAgent: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + force-reconcile: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + rules: + - apiGroups: + - "" + resources: + - endpoints + - services + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: sourcegraph-frontend + subjects: + - kind: ServiceAccount + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + endpoint: ZXhhbXBsZS5jb20= + kind: Secret + metadata: + creationTimestamp: "2024-04-19T00:00:00Z" + name: redis-store + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + type: Opaque + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + prometheus.io/port: "6060" + sourcegraph.prometheus/scrape: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend + app.kubernetes.io/component: sourcegraph-frontend + deploy: sourcegraph + name: sourcegraph-frontend + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 30080 + protocol: TCP + targetPort: http + selector: + app: sourcegraph-appliance + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} + - apiVersion: v1 + kind: Service + metadata: + annotations: + appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + app: sourcegraph-frontend-internal + app.kubernetes.io/component: sourcegraph-frontend-internal + deploy: sourcegraph + name: sourcegraph-frontend-internal + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + clusterIP: NORMALIZED_FOR_TESTING + clusterIPs: + - NORMALIZED_FOR_TESTING + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http-internal + port: 80 + protocol: TCP + targetPort: http-internal + selector: + app: sourcegraph-frontend + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/default.yaml index b4ed085855af3..bab50fc35d5f0 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/default.yaml @@ -3,7 +3,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + appliance.sourcegraph.com/configHash: 8f6b44deb3ec355b4074331d35e5b9d4e87c47388f3a8325a3fc50619bddc76d creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -38,6 +38,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -333,7 +335,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -465,6 +470,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -498,7 +504,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-blobstore.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-blobstore.yaml index 665251ffb99fd..f1ea98941a0fd 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-blobstore.yaml @@ -92,7 +92,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 + appliance.sourcegraph.com/configHash: 8f6b44deb3ec355b4074331d35e5b9d4e87c47388f3a8325a3fc50619bddc76d creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -127,6 +127,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -421,7 +423,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -623,6 +628,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: b5dce290e22d1afb4c9102ac4c245490ab01dd3be13653de391536cfe0e323b0 prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -656,7 +662,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress-optional-fields.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress-optional-fields.yaml index e1fba1efa90b4..18bfee8c16dce 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress-optional-fields.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress-optional-fields.yaml @@ -3,7 +3,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: 7caff941f7756c1a8aa77fb1604c7c1d191868889bbe1ca514eac63a4c1aafc8 + appliance.sourcegraph.com/configHash: c0cbf7fe1f7e4042a42aefd275ab15334b25954c53cb531eaa11f65e28a5d8f7 creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -38,6 +38,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -339,7 +341,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -471,6 +476,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: 7caff941f7756c1a8aa77fb1604c7c1d191868889bbe1ca514eac63a4c1aafc8 prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -504,7 +510,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: @@ -553,7 +559,8 @@ resources: kind: Ingress metadata: annotations: - appliance.sourcegraph.com/configHash: 7caff941f7756c1a8aa77fb1604c7c1d191868889bbe1ca514eac63a4c1aafc8 + appliance.sourcegraph.com/adopted: "true" + appliance.sourcegraph.com/configHash: f7d0ef6a7aa5f52fdd5bcf934843ccc4f023ec7eef42fa22a1a58ace16a2fa70 foo: bar creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress.yaml index 188181f39552d..105123e2c4093 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-ingress.yaml @@ -3,7 +3,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c + appliance.sourcegraph.com/configHash: eca9597d0eccefd3eb40039ed5d5afaf1b81989733f82692b933206540242d93 creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -38,6 +38,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -334,7 +336,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -466,6 +471,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -499,7 +505,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: @@ -548,6 +554,7 @@ resources: kind: Ingress metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: d3eb623947fedba566dfc56adc4733ff5ef1b2887a5cb63d75dbc1df452b0b5c creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-overrides.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-overrides.yaml index cc6a8b2e94b87..4baaf1a7f39f2 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-overrides.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/frontend/with-overrides.yaml @@ -3,7 +3,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: f6325ffd3262b0ff8bdb406ba80aabad2daea7ecc342353fabd7bbda7ea1f4a9 + appliance.sourcegraph.com/configHash: 555e852dff2a146934b0802c149b41ab07a0cf2f8890c33f7205344b3b9e6861 creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -38,6 +38,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -237,7 +239,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -369,6 +374,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: f6325ffd3262b0ff8bdb406ba80aabad2daea7ecc342353fabd7bbda7ea1f4a9 prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -402,7 +408,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/gitserver/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/gitserver/default.yaml index bda5b2e256838..09e4aaf3e6639 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/gitserver/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/gitserver/default.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: gitserver creationTimestamp: null labels: @@ -163,7 +164,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/default.yaml index 411bc6fb10f22..eed81ef3d76fd 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/default.yaml @@ -170,7 +170,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-existing-configmap.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-existing-configmap.yaml index 0aebbfc96a906..29ea3b1b0f2ff 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-existing-configmap.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-existing-configmap.yaml @@ -134,7 +134,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-replicas.yaml index 7c518ccb22dee..f2330b4fa8598 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/grafana/with-replicas.yaml @@ -170,7 +170,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/default.yaml index cedaee217b91b..b522ea35e1845 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/default.yaml @@ -186,7 +186,10 @@ resources: indexedSearch: {} - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/with-replicas.yaml index af7bf55ad87bf..e6e41be20f859 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/indexed-search/with-replicas.yaml @@ -187,7 +187,10 @@ resources: indexedSearch: replicas: 7 - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/default.yaml index 3e3a430ba33a1..b13cda7e85091 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/default.yaml @@ -125,7 +125,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/with-replicas.yaml index 4817352964e7f..7597e6a72d823 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/jaeger/with-replicas.yaml @@ -125,7 +125,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/otel-agent/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/otel-agent/default.yaml new file mode 100644 index 0000000000000..ea0caa13633a0 --- /dev/null +++ b/internal/appliance/reconciler/testdata/golden-fixtures/otel-agent/default.yaml @@ -0,0 +1,284 @@ +resources: + - apiVersion: apps/v1 + kind: DaemonSet + metadata: + annotations: + appliance.sourcegraph.com/configHash: c84ac276e60e85a888146303bb5d592cb42507dc290a515051c6ecedc03d6f4c + deprecated.daemonset.template.generation: "1" + creationTimestamp: "2024-04-19T00:00:00Z" + generation: 1 + labels: + app.kubernetes.io/component: otel-agent + app.kubernetes.io/name: sourcegraph + app.kubernetes.io/version: 5.3.9104 + deploy: sourcegraph + name: otel-agent + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + spec: + minReadySeconds: 10 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: otel-agent + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: otel-agent + creationTimestamp: null + labels: + app: otel-agent + deploy: sourcegraph + name: otel-agent + spec: + containers: + - command: + - /bin/otelcol-sourcegraph + - --config=/etc/otel-agent/config.yaml + image: index.docker.io/sourcegraph/opentelemetry-collector:5.3.9104 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 13133 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: otel-agent + ports: + - containerPort: 55679 + hostPort: 55679 + name: zpages + protocol: TCP + - containerPort: 4317 + hostPort: 4317 + name: otel-grpc + protocol: TCP + - containerPort: 4318 + hostPort: 4318 + name: otel-http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 13133 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 500m + memory: 500Mi + requests: + cpu: 100m + memory: 100Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsUser: 100 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: FallbackToLogsOnError + volumeMounts: + - mountPath: /etc/otel-agent + name: config + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 101 + runAsUser: 100 + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 511 + items: + - key: config.yaml + path: config.yaml + name: otel-agent + name: config + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 0 + desiredNumberScheduled: 0 + numberMisscheduled: 0 + numberReady: 0 + - apiVersion: v1 + data: + config.yaml: | + receivers: + otlp: + protocols: + grpc: # port 4317 + http: # port 4318 + + exporters: + otlp: + endpoint: "otel-collector:4317" + tls: + insecure: true + sending_queue: + num_consumers: 4 + queue_size: 100 + retry_on_failure: + enabled: true + + # TODO: allow configuring processors through values + #processors: + # batch: + # memory_limiter: + # # 80% of maximum memory up to 2G + # limit_mib: 400 + # # 25% of limit up to 2G + # spike_limit_mib: 100 + # check_interval: 5s + + extensions: + health_check: + endpoint: ":13133" + zpages: + endpoint: "localhost:55679" + + service: + extensions: + - zpages + - health_check + pipelines: + traces: + receivers: + - otlp + exporters: + - otlp + immutable: false + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/configHash: c84ac276e60e85a888146303bb5d592cb42507dc290a515051c6ecedc03d6f4c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: otel-agent + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + data: + spec: | + spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: + disabled: true + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryAgent: {} + + openTelemetryCollector: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisExporter: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true + kind: ConfigMap + metadata: + annotations: + appliance.sourcegraph.com/currentVersion: 5.3.9104 + appliance.sourcegraph.com/managed: "true" + creationTimestamp: "2024-04-19T00:00:00Z" + name: sg + namespace: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING + - apiVersion: v1 + kind: ServiceAccount + metadata: + annotations: + appliance.sourcegraph.com/configHash: c84ac276e60e85a888146303bb5d592cb42507dc290a515051c6ecedc03d6f4c + creationTimestamp: "2024-04-19T00:00:00Z" + labels: + deploy: sourcegraph + name: otel-agent + namespace: NORMALIZED_FOR_TESTING + ownerReferences: + - apiVersion: v1 + blockOwnerDeletion: true + controller: true + kind: ConfigMap + name: sg + uid: NORMALIZED_FOR_TESTING + resourceVersion: NORMALIZED_FOR_TESTING + uid: NORMALIZED_FOR_TESTING diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/pgsql/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/pgsql/default.yaml index 70b087f4905c3..41c4bd69596c0 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/pgsql/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/pgsql/default.yaml @@ -330,7 +330,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: {} diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/default.yaml index c960fa06d6a38..ccc200cde627c 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/default.yaml @@ -147,7 +147,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-blobstore.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-blobstore.yaml index ad64001dea0be..01df4e2168f47 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-blobstore.yaml @@ -239,7 +239,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-num-workers.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-num-workers.yaml index a3cfb98ffcb90..322ceb5c885ff 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-num-workers.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-num-workers.yaml @@ -147,7 +147,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-replicas.yaml index 53ba6628e2796..17d47d7f4e0bf 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/precise-code-intel/with-replicas.yaml @@ -147,7 +147,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/default.yaml index daf69eec30dc4..ba6fa3cd50acd 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/default.yaml @@ -369,7 +369,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/privileged.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/privileged.yaml index 839ba9011593a..4528c1145af06 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/privileged.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/privileged.yaml @@ -497,7 +497,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/subsequent-disable.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/subsequent-disable.yaml index 75f26a9075c88..0fa1de2e821cd 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/subsequent-disable.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/subsequent-disable.yaml @@ -85,7 +85,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/with-existing-configmap.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/with-existing-configmap.yaml index f663e29e78585..5788a757ba13e 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/with-existing-configmap.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/prometheus/with-existing-configmap.yaml @@ -125,7 +125,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/redis/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/redis/default.yaml index fea6280d2f607..a1a39c05078f3 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/redis/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/redis/default.yaml @@ -292,7 +292,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/repo-updater/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/repo-updater/default.yaml index d5702b0f62297..db7d2314133cd 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/repo-updater/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/repo-updater/default.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: repo-updater creationTimestamp: null labels: @@ -145,7 +146,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/default.yaml index af4be5fb60de6..fb2c2f59a3a40 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/default.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: searcher creationTimestamp: null labels: @@ -176,7 +177,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-replicas.yaml index 72b18c1b80865..50d684be0f172 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-replicas.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: searcher creationTimestamp: null labels: @@ -176,7 +177,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-storage.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-storage.yaml index ff79b3242e667..23f4342bccd87 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-storage.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/searcher/with-storage.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: searcher creationTimestamp: null labels: @@ -176,7 +177,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-subsequent-disable.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-subsequent-disable.yaml index 0b46af11d9d03..bae00eb21d33a 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-subsequent-disable.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-subsequent-disable.yaml @@ -26,7 +26,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-with-named-storage-class.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-with-named-storage-class.yaml index 0719a8e0141c0..8316b4ba86dfe 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-with-named-storage-class.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/blobstore-with-named-storage-class.yaml @@ -116,7 +116,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/frontend-with-no-cpu-memory-resources.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/frontend-with-no-cpu-memory-resources.yaml index 859469da34839..f988444d7438f 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/frontend-with-no-cpu-memory-resources.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/frontend-with-no-cpu-memory-resources.yaml @@ -3,7 +3,7 @@ resources: kind: Deployment metadata: annotations: - appliance.sourcegraph.com/configHash: e9e0837cc01eaabff90d9772835de8fbf79f814f32b58b2bac75ef8bfbc1d93d + appliance.sourcegraph.com/configHash: ee9f46b5b3822109e7eb4903d5cacba5f88db2d2c3481fdc93114346e92279c3 creationTimestamp: "2024-04-19T00:00:00Z" generation: 1 labels: @@ -38,6 +38,8 @@ resources: template: metadata: annotations: + checksum/auth: 64bb092df26e6c62383322ffa1cedee5083dbd8bfeba3e4a2f29492c1d8abfa4 + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: sourcegraph-frontend creationTimestamp: null labels: @@ -328,7 +330,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: @@ -460,6 +465,7 @@ resources: kind: Service metadata: annotations: + appliance.sourcegraph.com/adopted: "true" appliance.sourcegraph.com/configHash: e9e0837cc01eaabff90d9772835de8fbf79f814f32b58b2bac75ef8bfbc1d93d prometheus.io/port: "6060" sourcegraph.prometheus/scrape: "true" @@ -493,7 +499,7 @@ resources: protocol: TCP targetPort: http selector: - app: sourcegraph-frontend + app: sourcegraph-appliance sessionAffinity: None type: ClusterIP status: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/precise-code-intel-with-env-vars.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/precise-code-intel-with-env-vars.yaml index d981812a4f684..36eafcc25d5b1 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/precise-code-intel-with-env-vars.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/precise-code-intel-with-env-vars.yaml @@ -151,7 +151,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-multiple-custom-images.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-multiple-custom-images.yaml index 8b1d71b2f05c9..9553a6ee1274b 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-multiple-custom-images.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-multiple-custom-images.yaml @@ -159,7 +159,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-storage.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-storage.yaml index f5a587276cbba..4eff27961c597 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-storage.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/redis-with-storage.yaml @@ -292,7 +292,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-no-resources.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-no-resources.yaml index f544e614d2cd7..b4e48c5bf1b49 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-no-resources.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-no-resources.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: repo-updater creationTimestamp: null labels: @@ -139,7 +140,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-pod-template-config.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-pod-template-config.yaml index 019c9c8829c16..f09d3cc4f4292 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-pod-template-config.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-pod-template-config.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: repo-updater creationTimestamp: null labels: @@ -163,7 +164,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-resources.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-resources.yaml index 3421373c96aed..9e19f7abf68a0 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-resources.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-resources.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: repo-updater creationTimestamp: null labels: @@ -145,7 +146,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-sa-annotations.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-sa-annotations.yaml index 63993086d198a..177a05e32d466 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-sa-annotations.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/repo-updater-with-sa-annotations.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: repo-updater creationTimestamp: null labels: @@ -145,7 +146,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/standard/symbols-with-custom-image.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/standard/symbols-with-custom-image.yaml index 4748e5f222f62..763bea24a8259 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/standard/symbols-with-custom-image.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/standard/symbols-with-custom-image.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: symbols creationTimestamp: null labels: @@ -190,7 +191,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/symbols/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/symbols/default.yaml index 83bdb150e5c47..8bdcc08682415 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/symbols/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/symbols/default.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: symbols creationTimestamp: null labels: @@ -189,7 +190,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/symbols/with-storage.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/symbols/with-storage.yaml index 0804358da3289..d10169775a321 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/symbols/with-storage.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/symbols/with-storage.yaml @@ -37,6 +37,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: symbols creationTimestamp: null labels: @@ -189,7 +190,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/syntect/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/syntect/default.yaml index be1c2877af154..eb3973257a090 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/syntect/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/syntect/default.yaml @@ -123,7 +123,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/syntect/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/syntect/with-replicas.yaml index fe034478bd8aa..71d62e0a12e62 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/syntect/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/syntect/with-replicas.yaml @@ -123,7 +123,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/worker/default.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/worker/default.yaml index 858492c61d3e8..a790f9fb9c7b2 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/worker/default.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/worker/default.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: worker creationTimestamp: null labels: @@ -152,7 +153,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-blobstore.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-blobstore.yaml index 18a7c56c5feae..b73a241ba0f46 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-blobstore.yaml @@ -127,6 +127,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: worker creationTimestamp: null labels: @@ -244,7 +245,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-replicas.yaml b/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-replicas.yaml index 99c4f2b9891f9..ad03f22635709 100644 --- a/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/golden-fixtures/worker/with-replicas.yaml @@ -38,6 +38,7 @@ resources: template: metadata: annotations: + checksum/redis: 44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a kubectl.kubernetes.io/default-container: worker creationTimestamp: null labels: @@ -152,7 +153,10 @@ resources: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/blobstore/default.yaml b/internal/appliance/reconciler/testdata/sg/blobstore/default.yaml index dff8e38a54071..20f0730fc1a12 100644 --- a/internal/appliance/reconciler/testdata/sg/blobstore/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/blobstore/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/cadvisor/default.yaml b/internal/appliance/reconciler/testdata/sg/cadvisor/default.yaml index 8829b5f56e55c..d29717b8f5beb 100644 --- a/internal/appliance/reconciler/testdata/sg/cadvisor/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/cadvisor/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/codeinsights/default.yaml b/internal/appliance/reconciler/testdata/sg/codeinsights/default.yaml index 9e9329ad94635..984c4c3da1a24 100644 --- a/internal/appliance/reconciler/testdata/sg/codeinsights/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/codeinsights/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/codeintel/default.yaml b/internal/appliance/reconciler/testdata/sg/codeintel/default.yaml index 77f210f58fe30..6abf13fe5fe92 100644 --- a/internal/appliance/reconciler/testdata/sg/codeintel/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/codeintel/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/frontend/default.yaml b/internal/appliance/reconciler/testdata/sg/frontend/default.yaml index b0ec3c41cde57..5580909bfd567 100644 --- a/internal/appliance/reconciler/testdata/sg/frontend/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/frontend/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/frontend/with-blobstore.yaml b/internal/appliance/reconciler/testdata/sg/frontend/with-blobstore.yaml index dcfc56f7d915d..30e6d336804ae 100644 --- a/internal/appliance/reconciler/testdata/sg/frontend/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/sg/frontend/with-blobstore.yaml @@ -20,7 +20,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/frontend/with-ingress-optional-fields.yaml b/internal/appliance/reconciler/testdata/sg/frontend/with-ingress-optional-fields.yaml index 81e2cc0e273f9..09ed7a3f74b7f 100644 --- a/internal/appliance/reconciler/testdata/sg/frontend/with-ingress-optional-fields.yaml +++ b/internal/appliance/reconciler/testdata/sg/frontend/with-ingress-optional-fields.yaml @@ -27,7 +27,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/frontend/with-ingress.yaml b/internal/appliance/reconciler/testdata/sg/frontend/with-ingress.yaml index 0280afac18092..76ea9a413e087 100644 --- a/internal/appliance/reconciler/testdata/sg/frontend/with-ingress.yaml +++ b/internal/appliance/reconciler/testdata/sg/frontend/with-ingress.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/frontend/with-overrides.yaml b/internal/appliance/reconciler/testdata/sg/frontend/with-overrides.yaml index e8d943b694b3d..2f6c8042dc0a5 100644 --- a/internal/appliance/reconciler/testdata/sg/frontend/with-overrides.yaml +++ b/internal/appliance/reconciler/testdata/sg/frontend/with-overrides.yaml @@ -23,7 +23,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/gitserver/default.yaml b/internal/appliance/reconciler/testdata/sg/gitserver/default.yaml index c5e64b0924aa9..b6b0da95de5ac 100644 --- a/internal/appliance/reconciler/testdata/sg/gitserver/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/gitserver/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/grafana/default.yaml b/internal/appliance/reconciler/testdata/sg/grafana/default.yaml index 71af2f2f7699b..33de6a64b17f1 100644 --- a/internal/appliance/reconciler/testdata/sg/grafana/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/grafana/default.yaml @@ -19,7 +19,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/grafana/with-existing-configmap.yaml b/internal/appliance/reconciler/testdata/sg/grafana/with-existing-configmap.yaml index 5c55cbd86fbb9..ffdfed0a8ef48 100644 --- a/internal/appliance/reconciler/testdata/sg/grafana/with-existing-configmap.yaml +++ b/internal/appliance/reconciler/testdata/sg/grafana/with-existing-configmap.yaml @@ -19,7 +19,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/grafana/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/grafana/with-replicas.yaml index cf487fd9edb53..9347436b11b64 100644 --- a/internal/appliance/reconciler/testdata/sg/grafana/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/grafana/with-replicas.yaml @@ -19,7 +19,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/indexed-search/default.yaml b/internal/appliance/reconciler/testdata/sg/indexed-search/default.yaml index 081f8627dda81..9520aa5f4d9c5 100644 --- a/internal/appliance/reconciler/testdata/sg/indexed-search/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/indexed-search/default.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: {} - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/indexed-search/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/indexed-search/with-replicas.yaml index 42eda55e6f0d0..70f89015e32c4 100644 --- a/internal/appliance/reconciler/testdata/sg/indexed-search/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/indexed-search/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: replicas: 7 - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/jaeger/default.yaml b/internal/appliance/reconciler/testdata/sg/jaeger/default.yaml index 4818db259b479..d5e31bb9d4d6f 100644 --- a/internal/appliance/reconciler/testdata/sg/jaeger/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/jaeger/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/jaeger/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/jaeger/with-replicas.yaml index e068415e71def..3937a24e0b2c9 100644 --- a/internal/appliance/reconciler/testdata/sg/jaeger/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/jaeger/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/otel-agent/default.yaml b/internal/appliance/reconciler/testdata/sg/otel-agent/default.yaml new file mode 100644 index 0000000000000..042606e4ec739 --- /dev/null +++ b/internal/appliance/reconciler/testdata/sg/otel-agent/default.yaml @@ -0,0 +1,64 @@ +spec: + requestedVersion: "5.3.9104" + + blobstore: + disabled: true + + codeInsights: + disabled: true + + codeIntel: + disabled: true + + frontend: + disabled: true + + grafana: + disabled: true + + gitServer: + disabled: true + + indexedSearch: + disabled: true + + openTelemetryAgent: {} + + openTelemetryCollector: + disabled: true + + pgsql: + disabled: true + + postgresExporter: + disabled: true + + preciseCodeIntel: + disabled: true + + redisCache: + disabled: true + + redisExporter: + disabled: true + + redisStore: + disabled: true + + repoUpdater: + disabled: true + + searcher: + disabled: true + + symbols: + disabled: true + + syntectServer: + disabled: true + + worker: + disabled: true + + prometheus: + disabled: true diff --git a/internal/appliance/reconciler/testdata/sg/pgsql/default.yaml b/internal/appliance/reconciler/testdata/sg/pgsql/default.yaml index d701ae845b0f6..676a27cabf6c4 100644 --- a/internal/appliance/reconciler/testdata/sg/pgsql/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/pgsql/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: {} diff --git a/internal/appliance/reconciler/testdata/sg/precise-code-intel/default.yaml b/internal/appliance/reconciler/testdata/sg/precise-code-intel/default.yaml index 08e6bc1634c19..ae4b6d9bc0d51 100644 --- a/internal/appliance/reconciler/testdata/sg/precise-code-intel/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/precise-code-intel/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-blobstore.yaml b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-blobstore.yaml index 8652a7716ff0a..44dae1db635c1 100644 --- a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-blobstore.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-num-workers.yaml b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-num-workers.yaml index 6b63354c79408..161a189fa236f 100644 --- a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-num-workers.yaml +++ b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-num-workers.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-replicas.yaml index bedd1dab89c51..692bb4ffbe293 100644 --- a/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/precise-code-intel/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/prometheus/default.yaml b/internal/appliance/reconciler/testdata/sg/prometheus/default.yaml index f2e579c99c2d6..31f410160ea83 100644 --- a/internal/appliance/reconciler/testdata/sg/prometheus/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/prometheus/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/prometheus/privileged.yaml b/internal/appliance/reconciler/testdata/sg/prometheus/privileged.yaml index baca7f8b9e2b0..07ee7cd400b74 100644 --- a/internal/appliance/reconciler/testdata/sg/prometheus/privileged.yaml +++ b/internal/appliance/reconciler/testdata/sg/prometheus/privileged.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/prometheus/with-existing-configmap.yaml b/internal/appliance/reconciler/testdata/sg/prometheus/with-existing-configmap.yaml index 1baa5dd9fd29a..72c2c8506f4f4 100644 --- a/internal/appliance/reconciler/testdata/sg/prometheus/with-existing-configmap.yaml +++ b/internal/appliance/reconciler/testdata/sg/prometheus/with-existing-configmap.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/redis/default.yaml b/internal/appliance/reconciler/testdata/sg/redis/default.yaml index f6cb8d354559a..8823de7e5f7b7 100644 --- a/internal/appliance/reconciler/testdata/sg/redis/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/redis/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/repo-updater/default.yaml b/internal/appliance/reconciler/testdata/sg/repo-updater/default.yaml index c97f218fa49ba..f880d69506e16 100644 --- a/internal/appliance/reconciler/testdata/sg/repo-updater/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/repo-updater/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/searcher/default.yaml b/internal/appliance/reconciler/testdata/sg/searcher/default.yaml index 5a5350a75be2c..58f4e5adadd09 100644 --- a/internal/appliance/reconciler/testdata/sg/searcher/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/searcher/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/searcher/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/searcher/with-replicas.yaml index 45006c178ff05..048fdbaf8e4b8 100644 --- a/internal/appliance/reconciler/testdata/sg/searcher/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/searcher/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/searcher/with-storage.yaml b/internal/appliance/reconciler/testdata/sg/searcher/with-storage.yaml index 10c24d2d10506..d36320ddf2154 100644 --- a/internal/appliance/reconciler/testdata/sg/searcher/with-storage.yaml +++ b/internal/appliance/reconciler/testdata/sg/searcher/with-storage.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/blobstore-with-named-storage-class.yaml b/internal/appliance/reconciler/testdata/sg/standard/blobstore-with-named-storage-class.yaml index 57f6e8035ff44..12f11a3e25fdc 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/blobstore-with-named-storage-class.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/blobstore-with-named-storage-class.yaml @@ -23,7 +23,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/everything-disabled.yaml b/internal/appliance/reconciler/testdata/sg/standard/everything-disabled.yaml index d5fd38622942c..9ed4cef3918fa 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/everything-disabled.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/everything-disabled.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/frontend-with-no-cpu-memory-resources.yaml b/internal/appliance/reconciler/testdata/sg/standard/frontend-with-no-cpu-memory-resources.yaml index 07f5d793bdcaf..2fa8787ce2aaf 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/frontend-with-no-cpu-memory-resources.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/frontend-with-no-cpu-memory-resources.yaml @@ -26,7 +26,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/precise-code-intel-with-env-vars.yaml b/internal/appliance/reconciler/testdata/sg/standard/precise-code-intel-with-env-vars.yaml index e30dcc4f7fc9a..14b7064245abe 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/precise-code-intel-with-env-vars.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/precise-code-intel-with-env-vars.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/redis-with-multiple-custom-images.yaml b/internal/appliance/reconciler/testdata/sg/standard/redis-with-multiple-custom-images.yaml index 84d184aed7f1b..4ef11ba7aedd2 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/redis-with-multiple-custom-images.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/redis-with-multiple-custom-images.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/redis-with-storage.yaml b/internal/appliance/reconciler/testdata/sg/standard/redis-with-storage.yaml index fc768d462d462..a347081c10abb 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/redis-with-storage.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/redis-with-storage.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-no-resources.yaml b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-no-resources.yaml index a5b00baf9f2f7..5dc1989624833 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-no-resources.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-no-resources.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-pod-template-config.yaml b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-pod-template-config.yaml index 43456ffd2f943..0234b2f913768 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-pod-template-config.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-pod-template-config.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-resources.yaml b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-resources.yaml index 7797625a5ce57..47e3a63180bb3 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-resources.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-resources.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-sa-annotations.yaml b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-sa-annotations.yaml index 672806c9d9417..f284a7ebb775d 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-sa-annotations.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/repo-updater-with-sa-annotations.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/standard/symbols-with-custom-image.yaml b/internal/appliance/reconciler/testdata/sg/standard/symbols-with-custom-image.yaml index 737b29d12cb12..843794c0f5bd1 100644 --- a/internal/appliance/reconciler/testdata/sg/standard/symbols-with-custom-image.yaml +++ b/internal/appliance/reconciler/testdata/sg/standard/symbols-with-custom-image.yaml @@ -23,7 +23,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/symbols/default.yaml b/internal/appliance/reconciler/testdata/sg/symbols/default.yaml index 4fc0041da3a2a..c1c13b858d756 100644 --- a/internal/appliance/reconciler/testdata/sg/symbols/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/symbols/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/symbols/with-storage.yaml b/internal/appliance/reconciler/testdata/sg/symbols/with-storage.yaml index 0c88fd08868f7..b46091515f6b5 100644 --- a/internal/appliance/reconciler/testdata/sg/symbols/with-storage.yaml +++ b/internal/appliance/reconciler/testdata/sg/symbols/with-storage.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/syntect/default.yaml b/internal/appliance/reconciler/testdata/sg/syntect/default.yaml index 00877327e1423..a2a6b3296dcc5 100644 --- a/internal/appliance/reconciler/testdata/sg/syntect/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/syntect/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/syntect/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/syntect/with-replicas.yaml index 58ab19a441678..421ae4d70bddc 100644 --- a/internal/appliance/reconciler/testdata/sg/syntect/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/syntect/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/worker/default.yaml b/internal/appliance/reconciler/testdata/sg/worker/default.yaml index 35adba82c473c..4fc3aa47200b5 100644 --- a/internal/appliance/reconciler/testdata/sg/worker/default.yaml +++ b/internal/appliance/reconciler/testdata/sg/worker/default.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/worker/with-blobstore.yaml b/internal/appliance/reconciler/testdata/sg/worker/with-blobstore.yaml index 2369b3ca5e2da..a3ab5b4b02ea5 100644 --- a/internal/appliance/reconciler/testdata/sg/worker/with-blobstore.yaml +++ b/internal/appliance/reconciler/testdata/sg/worker/with-blobstore.yaml @@ -21,7 +21,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/testdata/sg/worker/with-replicas.yaml b/internal/appliance/reconciler/testdata/sg/worker/with-replicas.yaml index 2d80a321ca41d..88c4856a4268e 100644 --- a/internal/appliance/reconciler/testdata/sg/worker/with-replicas.yaml +++ b/internal/appliance/reconciler/testdata/sg/worker/with-replicas.yaml @@ -22,7 +22,10 @@ spec: indexedSearch: disabled: true - openTelemetry: + openTelemetryCollector: + disabled: true + + openTelemetryAgent: disabled: true pgsql: diff --git a/internal/appliance/reconciler/worker.go b/internal/appliance/reconciler/worker.go index e976f55bf2d4a..e34a9b1ac0d64 100644 --- a/internal/appliance/reconciler/worker.go +++ b/internal/appliance/reconciler/worker.go @@ -90,16 +90,32 @@ func (r *Reconciler) reconcileWorkerDeployment(ctx context.Context, sg *config.S podTemplate := pod.NewPodTemplate(name, cfg) podTemplate.Template.Spec.Containers = []corev1.Container{ctr} + redisConnSpecs, err := r.getRedisSecrets(ctx, sg) + if err != nil { + return err + } + redisConnHash, err := configHash(redisConnSpecs) + if err != nil { + return err + } + podTemplate.Template.ObjectMeta.Annotations["checksum/redis"] = redisConnHash dep := deployment.NewDeployment(name, sg.Namespace, sg.Spec.RequestedVersion) dep.Spec.Replicas = pointers.Ptr(cfg.Replicas) dep.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ - MaxSurge: pointers.Ptr(intstr.FromInt(1)), - MaxUnavailable: pointers.Ptr(intstr.FromInt(1)), + MaxSurge: pointers.Ptr(intstr.FromInt32(1)), + MaxUnavailable: pointers.Ptr(intstr.FromInt32(1)), } dep.Spec.Template = podTemplate.Template - return reconcileObject(ctx, r, cfg, &dep, &appsv1.Deployment{}, sg, owner) + ifChanged := struct { + config.WorkerSpec + RedisConnSpecs + }{ + WorkerSpec: cfg, + RedisConnSpecs: redisConnSpecs, + } + return reconcileObject(ctx, r, ifChanged, &dep, &appsv1.Deployment{}, sg, owner) } func (r *Reconciler) reconcileWorkerService(ctx context.Context, sg *config.Sourcegraph, owner client.Object) error { diff --git a/internal/appliance/routes.go b/internal/appliance/routes.go index 8aa0d4d645864..fd8f0657a5576 100644 --- a/internal/appliance/routes.go +++ b/internal/appliance/routes.go @@ -8,19 +8,17 @@ import ( func (a *Appliance) Routes() *mux.Router { r := mux.NewRouter() - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/appliance", http.StatusFound) - }) + r.Use(a.checkAuthorization) - r.Handle("/appliance/login", a.getLoginHandler()).Methods(http.MethodGet) - r.Handle("/appliance/login", a.postLoginHandler()).Methods(http.MethodPost) - r.Handle("/appliance/error", a.errorHandler()).Methods(http.MethodGet) + // Route errors + r.NotFoundHandler = http.HandlerFunc(a.notFoundResponse) + r.MethodNotAllowedHandler = http.HandlerFunc(a.methodNotAllowedResponse) - // Auth-gated endpoints - r.Handle("/appliance", a.CheckAuthorization(a.applianceHandler())).Methods(http.MethodGet) - r.Handle("/appliance/setup", a.CheckAuthorization(a.getSetupHandler())).Methods(http.MethodGet) - r.Handle("/appliance/setup", a.CheckAuthorization(a.postSetupHandler())).Methods(http.MethodPost) + // Maintenance API URIs + r.Handle("/api/v1/appliance/status", a.getStatusJSONHandler()).Methods("GET") + r.Handle("/api/v1/appliance/status", a.postStatusJSONHandler()).Methods("POST") + r.Handle("/api/v1/appliance/install/progress", a.getInstallProgressJSONHandler()).Methods("GET") + r.Handle("/api/v1/appliance/maintenance/serviceStatuses", a.getMaintenanceStatusHandler()).Methods("GET") return r } diff --git a/internal/appliance/selfupdate/BUILD.bazel b/internal/appliance/selfupdate/BUILD.bazel new file mode 100644 index 0000000000000..86d5bf9bcd550 --- /dev/null +++ b/internal/appliance/selfupdate/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_defs.bzl", "go_test") + +go_library( + name = "selfupdate", + srcs = ["selfupdate.go"], + importpath = "github.com/sourcegraph/sourcegraph/internal/appliance/selfupdate", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/appliance", + "//internal/releaseregistry", + "//lib/errors", + "@com_github_life4_genesis//slices", + "@com_github_sourcegraph_log//:log", + "@io_k8s_api//apps/v1:apps", + "@io_k8s_apimachinery//pkg/types", + "@io_k8s_sigs_controller_runtime//pkg/client", + ], +) + +go_test( + name = "selfupdate_test", + srcs = ["selfupdate_test.go"], + embed = [":selfupdate"], + deps = [ + "//internal/releaseregistry", + "//internal/releaseregistry/mocks", + "@com_github_sourcegraph_log//logtest", + "@com_github_stretchr_testify//require", + ], +) diff --git a/internal/appliance/selfupdate/integrationtest/BUILD.bazel b/internal/appliance/selfupdate/integrationtest/BUILD.bazel new file mode 100644 index 0000000000000..5dc4b47184ca5 --- /dev/null +++ b/internal/appliance/selfupdate/integrationtest/BUILD.bazel @@ -0,0 +1,31 @@ +load("//dev:go_defs.bzl", "go_test") + +go_test( + name = "integrationtest_test", + srcs = ["integration_test.go"], + data = [ + "//dev/tools:kubebuilder-assets", + ], + env = { + "KUBEBUILDER_ASSET_PATHS": "$(rlocationpaths //dev/tools:kubebuilder-assets)", + }, + deps = [ + "//internal/appliance/config", + "//internal/appliance/k8senvtest", + "//internal/appliance/selfupdate", + "//internal/k8s/resource/container", + "//internal/k8s/resource/deployment", + "//internal/k8s/resource/pod", + "//internal/releaseregistry", + "//internal/releaseregistry/mocks", + "@com_github_sourcegraph_log//:log", + "@com_github_sourcegraph_log//logtest", + "@com_github_sourcegraph_log_logr//:logr", + "@com_github_stretchr_testify//require", + "@io_k8s_api//apps/v1:apps", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_apimachinery//pkg/types", + "@io_k8s_sigs_controller_runtime//pkg/client", + ], +) diff --git a/internal/appliance/selfupdate/integrationtest/integration_test.go b/internal/appliance/selfupdate/integrationtest/integration_test.go new file mode 100644 index 0000000000000..a3fbfb28733ea --- /dev/null +++ b/internal/appliance/selfupdate/integrationtest/integration_test.go @@ -0,0 +1,150 @@ +package integrationtest + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/log" + "github.com/sourcegraph/log/logr" + "github.com/sourcegraph/log/logtest" + "github.com/sourcegraph/sourcegraph/internal/appliance/config" + "github.com/sourcegraph/sourcegraph/internal/appliance/k8senvtest" + "github.com/sourcegraph/sourcegraph/internal/appliance/selfupdate" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/container" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/deployment" + "github.com/sourcegraph/sourcegraph/internal/k8s/resource/pod" + "github.com/sourcegraph/sourcegraph/internal/releaseregistry" + "github.com/sourcegraph/sourcegraph/internal/releaseregistry/mocks" +) + +// Separate envtest-using things here, that have expensive setup and teardown, +// from the faster unit tests. + +var ( + // set once, before suite runs. See TestMain + ctx context.Context + k8sClient client.Client +) + +func TestMain(m *testing.M) { + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + + logger := log.Scoped("selfupdate-tests") + k8sConfig, cleanup, err := k8senvtest.SetupEnvtest(ctx, logr.New(logger), k8senvtest.NewNoopReconciler) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer func() { + if err := cleanup(); err != nil { + fmt.Println(err) + os.Exit(1) + } + }() + + k8sClient, err = client.New(k8sConfig, client.Options{}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + rc := m.Run() + + // Our earlier defer won't run after we call os.Exit() below + if err := cleanup(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + os.Exit(rc) +} + +func TestSelfUpdateLoop(t *testing.T) { + ns, err := k8senvtest.NewRandomNamespace("test-appliance-self-update") + require.NoError(t, err) + err = k8sClient.Create(ctx, ns) + require.NoError(t, err) + nsName := ns.GetName() + + // provision example appliance deployment + dep1 := buildTestDeployment("appliance", nsName) + err = k8sClient.Create(ctx, dep1) + require.NoError(t, err) + + dep2 := buildTestDeployment("appliance-frontend", nsName) + err = k8sClient.Create(ctx, dep2) + require.NoError(t, err) + + cfgMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.ConfigmapName, + Namespace: nsName, + Annotations: map[string]string{ + config.AnnotationKeyCurrentVersion: "4.3.1", + }, + }, + } + err = k8sClient.Create(ctx, cfgMap) + require.NoError(t, err) + + relregClient := mocks.NewMockReleaseRegistryClient() + relregClient.ListVersionsFunc.SetDefaultReturn([]releaseregistry.ReleaseInfo{ + {Version: "4.5.7", Public: true}, + }, nil) + selfUpdater := &selfupdate.SelfUpdate{ + Interval: time.Second, + Logger: logtest.Scoped(t), + RelregClient: relregClient, + K8sClient: k8sClient, + DeploymentNames: "appliance,appliance-frontend", + Namespace: nsName, + } + + loopCtx, cancel := context.WithCancel(context.Background()) + loopDone := make(chan struct{}) + go func() { + err := selfUpdater.Loop(loopCtx) + if !errors.Is(err, context.Canceled) { + require.NoError(t, err) + } + close(loopDone) + }() + + for _, depName := range []string{"appliance", "appliance-frontend"} { + require.Eventually(t, func() bool { + var dep appsv1.Deployment + depName := types.NamespacedName{Name: depName, Namespace: nsName} + require.NoError(t, k8sClient.Get(ctx, depName, &dep)) + return strings.HasSuffix(dep.Spec.Template.Spec.Containers[0].Image, "4.5.7") + }, time.Second*10, time.Second) + } + + cancel() + <-loopDone +} + +func buildTestDeployment(name, namespace string) *appsv1.Deployment { + defaultContainer := container.NewContainer(name, nil, config.ContainerConfig{ + Image: "index.docker.io/sourcegraph/appliance:4.3.1", + Resources: &corev1.ResourceRequirements{}, + }) + podTemplate := pod.NewPodTemplate(name, nil) + podTemplate.Template.Spec.Containers = []corev1.Container{defaultContainer} + defaultDeployment := deployment.NewDeployment(name, namespace, "4.3.1") + defaultDeployment.Spec.Template = podTemplate.Template + return &defaultDeployment +} diff --git a/internal/appliance/selfupdate/selfupdate.go b/internal/appliance/selfupdate/selfupdate.go new file mode 100644 index 0000000000000..e2e4eec03f6fb --- /dev/null +++ b/internal/appliance/selfupdate/selfupdate.go @@ -0,0 +1,107 @@ +package selfupdate + +import ( + "context" + "strings" + "time" + + "github.com/life4/genesis/slices" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/internal/appliance" + "github.com/sourcegraph/sourcegraph/internal/releaseregistry" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type SelfUpdate struct { + Interval time.Duration + Logger log.Logger + K8sClient client.Client + RelregClient releaseregistry.ReleaseRegistryClient + DeploymentNames string + Namespace string +} + +func (u *SelfUpdate) Loop(ctx context.Context) error { + u.Logger.Info("starting self-update loop") + + ticker := time.NewTicker(u.Interval) + defer ticker.Stop() + + // Do one iteration without having to wait for the first tick + if err := u.Once(ctx); err != nil { + u.Logger.Error("error self-updating", log.Error(err)) + return err + } + for { + select { + case <-ticker.C: + if err := u.Once(ctx); err != nil { + u.Logger.Error("error self-updating", log.Error(err)) + return err + } + case <-ctx.Done(): + u.Logger.Error("self-update context done, exiting", log.Error(ctx.Err())) + return ctx.Err() + } + } +} + +func (u *SelfUpdate) Once(ctx context.Context) error { + u.Logger.Info("starting self-update") + + var deps []appsv1.Deployment + for _, depName := range strings.Split(u.DeploymentNames, ",") { + depNsName := types.NamespacedName{Name: depName, Namespace: u.Namespace} + var dep appsv1.Deployment + if err := u.K8sClient.Get(ctx, depNsName, &dep); err != nil { + return errors.Wrap(err, "getting deployment") + } + deps = append(deps, dep) + } + + newTag, err := u.getLatestTag(ctx) + if err != nil { + return errors.Wrap(err, "getting latest tag") + } + + for _, dep := range deps { + dep.Spec.Template.Spec.Containers[0].Image = replaceTag(dep.Spec.Template.Spec.Containers[0].Image, newTag) + if err := u.K8sClient.Update(ctx, &dep); err != nil { + return errors.Wrap(err, "updating deployment") + } + } + + return nil +} + +func (u *SelfUpdate) getLatestTag(ctx context.Context) (string, error) { + versions, err := u.RelregClient.ListVersions(ctx, "sourcegraph") + if err != nil { + return "", err + } + versionStrs := slices.MapFilter(versions, func(version releaseregistry.ReleaseInfo) (string, bool) { + return version.Version, version.Public + }) + if len(versionStrs) == 0 { + return "", errors.New("no versions found") + } + semvers, err := appliance.ParseVersions(versionStrs) + if err != nil { + return "", errors.Wrap(err, "parsing versions from release registry") + } + latestVersion := semvers[len(semvers)-1].String() + + u.Logger.Info("found latest version", log.String("version", latestVersion)) + return latestVersion, nil +} + +// I thought about using regular expressions for this but I swear that's not +// better. +func replaceTag(image, newTag string) string { + imgParts := strings.Split(image, ":") + return strings.Join(imgParts[:len(imgParts)-1], ":") + ":" + newTag +} diff --git a/internal/appliance/selfupdate/selfupdate_test.go b/internal/appliance/selfupdate/selfupdate_test.go new file mode 100644 index 0000000000000..8eb061724ae1e --- /dev/null +++ b/internal/appliance/selfupdate/selfupdate_test.go @@ -0,0 +1,43 @@ +package selfupdate + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/sourcegraph/log/logtest" + "github.com/sourcegraph/sourcegraph/internal/releaseregistry" + "github.com/sourcegraph/sourcegraph/internal/releaseregistry/mocks" +) + +func TestReplaceTag(t *testing.T) { + img := "index.docker.io/sourcegraph/appliance:1.2.3" + updated := replaceTag(img, "4.5.6") + require.Equal(t, "index.docker.io/sourcegraph/appliance:4.5.6", updated) +} + +func TestReplaceTagNeverPanics(t *testing.T) { + img := "badImageNameFormat" + updated := replaceTag(img, "4.5.6") + require.Equal(t, ":4.5.6", updated) +} + +func TestGetLatestTag_ReturnsLatestSupportedPublicVersion(t *testing.T) { + relregClient := mocks.NewMockReleaseRegistryClient() + selfUpdater := &SelfUpdate{ + Logger: logtest.Scoped(t), + RelregClient: relregClient, + } + relregClient.ListVersionsFunc.PushReturn([]releaseregistry.ReleaseInfo{ + {Version: "v4.5.6", Public: false}, + {Version: "v4.5.5", Public: true}, + {Version: "v4.5.4", Public: true}, + {Version: "v4.5.3", Public: false}, + {Version: "v3.17.1", Public: true}, + }, nil) + + latest, err := selfUpdater.getLatestTag(context.Background()) + require.NoError(t, err) + require.Equal(t, "4.5.5", latest) +} diff --git a/internal/appliance/status.go b/internal/appliance/status.go new file mode 100644 index 0000000000000..0e4a89e8486ec --- /dev/null +++ b/internal/appliance/status.go @@ -0,0 +1,128 @@ +package appliance + +import ( + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// Task is a task that some states may have to complete to exit. +type Task struct { + Title string `json:"title"` + Description string `json:"description"` + Started bool `json:"started"` + Finished bool `json:"finished"` + Weight int `json:"weight"` + Progress int `json:"progress"` + LastUpdate time.Time `json:"lastUpdate"` +} + +func installTasks() []Task { + return []Task{ + { + Title: "Setup", + Description: "Setting up Sourcegraph Search", + Started: false, + Finished: false, + Weight: 25, + }, + } +} + +// IsObjectReady checks if a k8s object is ready, with the definition of ready depending on the object type. +// Supported resource types are StatefulSets, Deployments, and PersistentVolumeClaims. +func IsObjectReady(obj client.Object) (bool, error) { + switch o := obj.(type) { + case *appsv1.StatefulSet: + return IsStatefulSetReady(o) + case *appsv1.Deployment: + return IsDeploymentReady(o) + case *corev1.PersistentVolumeClaim: + return IsPersistentVolumeClaimReady(o) + default: + return false, errors.Newf("unsupported resource type: %T", obj) + } +} + +// IsStatefulSetReady checks if a StatefulSet is ready +func IsStatefulSetReady(sts *appsv1.StatefulSet) (bool, error) { + if sts.Status.ReadyReplicas != *sts.Spec.Replicas { + return false, nil + } + // StatefulSet controller has not processed most recent changes + if sts.Status.ObservedGeneration < sts.Generation { + return false, nil + } + // StatefulSet controller has not updated all pods to the latest version + if sts.Status.CurrentRevision != sts.Status.UpdateRevision { + return false, nil + } + return true, nil +} + +// IsDeploymentReady checks if a Deployment is ready +func IsDeploymentReady(deploy *appsv1.Deployment) (bool, error) { + if deploy.Status.ReadyReplicas != *deploy.Spec.Replicas { + return false, nil + } + for _, condition := range deploy.Status.Conditions { + if condition.Type == appsv1.DeploymentAvailable && condition.Status == corev1.ConditionTrue { + return true, nil + } + } + return false, nil +} + +// IsPersistentVolumeClaimReady checks if a PersistentVolumeClaim is ready +func IsPersistentVolumeClaimReady(pvc *corev1.PersistentVolumeClaim) (bool, error) { + return pvc.Status.Phase == corev1.ClaimBound, nil +} + +func calculateProgress(tasks []Task) ([]Task, int) { + totalWeight := sumTaskWeights(tasks) + progress := calculateTotalProgress(tasks) + overallProgress := calculateOverallProgress(progress, totalWeight) + + return tasks, overallProgress +} + +func sumTaskWeights(tasks []Task) int { + total := 0 + for _, task := range tasks { + total += task.Weight + } + return total +} + +func calculateTotalProgress(tasks []Task) float32 { + var progress float32 + for _, task := range tasks { + progress += calculateTaskProgress(task) + } + return progress +} + +func calculateTaskProgress(task Task) float32 { + if task.Finished { + return float32(task.Weight) + } + if task.Started { + return float32(task.Weight) * clampProgress(task.Progress) / 100 + } + return 0 +} + +func clampProgress(progress int) float32 { + if progress > 100 { + return 100 + } + return float32(progress) +} + +func calculateOverallProgress(progress float32, totalWeight int) int { + return int(progress / float32(totalWeight) * 100) +} diff --git a/internal/appliance/status_test.go b/internal/appliance/status_test.go new file mode 100644 index 0000000000000..5befd870de30d --- /dev/null +++ b/internal/appliance/status_test.go @@ -0,0 +1,158 @@ +package appliance + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestIsResourceReady(t *testing.T) { + tests := []struct { + name string + obj client.Object + expected bool + wantErr bool + }{ + { + name: "Ready StatefulSet", + obj: createReadyStatefulSet(), + expected: true, + wantErr: false, + }, + { + name: "Not Ready StatefulSet", + obj: createNotReadyStatefulSet(), + expected: false, + wantErr: false, + }, + { + name: "Ready Deployment", + obj: createReadyDeployment(), + expected: true, + wantErr: false, + }, + { + name: "Not Ready Deployment", + obj: createNotReadyDeployment(), + expected: false, + wantErr: false, + }, + { + name: "Ready PersistentVolumeClaim", + obj: createReadyPVC(), + expected: true, + wantErr: false, + }, + { + name: "Not Ready PersistentVolumeClaim", + obj: createNotReadyPVC(), + expected: false, + wantErr: false, + }, + { + name: "Unsupported Resource Type", + obj: &corev1.Pod{}, + expected: false, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := IsObjectReady(tt.obj) + if (err != nil) != tt.wantErr { + t.Errorf("IsObjectReady() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.expected { + t.Errorf("IsObjectReady() = %v, want %v", got, tt.expected) + } + }) + } +} + +func createReadyStatefulSet() *appsv1.StatefulSet { + replicas := int32(3) + return &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 3, + ObservedGeneration: 1, + CurrentRevision: "rev1", + UpdateRevision: "rev1", + }, + } +} + +func createNotReadyStatefulSet() *appsv1.StatefulSet { + replicas := int32(3) + return &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + ReadyReplicas: 2, + ObservedGeneration: 1, + CurrentRevision: "rev1", + UpdateRevision: "rev1", + }, + } +} + +func createReadyDeployment() *appsv1.Deployment { + replicas := int32(3) + return &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + Status: appsv1.DeploymentStatus{ + ReadyReplicas: 3, + Conditions: []appsv1.DeploymentCondition{ + { + Type: appsv1.DeploymentAvailable, + Status: corev1.ConditionTrue, + }, + }, + }, + } +} + +func createNotReadyDeployment() *appsv1.Deployment { + replicas := int32(3) + return &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + Status: appsv1.DeploymentStatus{ + ReadyReplicas: 2, + Conditions: []appsv1.DeploymentCondition{ + { + Type: appsv1.DeploymentAvailable, + Status: corev1.ConditionTrue, + }, + }, + }, + } +} + +func createReadyPVC() *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + }, + } +} + +func createNotReadyPVC() *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + } +} diff --git a/internal/appliance/upgrades/BUILD.bazel b/internal/appliance/upgrades/BUILD.bazel index 7532f40059bb7..8abee4ce101fd 100644 --- a/internal/appliance/upgrades/BUILD.bazel +++ b/internal/appliance/upgrades/BUILD.bazel @@ -8,6 +8,10 @@ go_library( tags = [TAG_INFRA_RELEASE], visibility = ["//:__subpackages__"], deps = [ + "//internal/database/connections/live", + "//internal/database/migration/schemas", + "//internal/database/postgresdsn", + "//internal/observation", "//internal/version", "//lib/errors", "@com_github_masterminds_semver//:semver", @@ -18,4 +22,9 @@ go_test( name = "upgrades_test", srcs = ["util_test.go"], embed = [":upgrades"], + tags = ["requires-network"], + deps = [ + "//internal/database/dbtest", + "//internal/observation", + ], ) diff --git a/internal/appliance/upgrades/util.go b/internal/appliance/upgrades/util.go index 8ae2c587e0d94..dc10d6b93cad5 100644 --- a/internal/appliance/upgrades/util.go +++ b/internal/appliance/upgrades/util.go @@ -3,10 +3,15 @@ package upgrades // This file contains handler logic for appliances upgrades. import ( + "database/sql" "fmt" "github.com/Masterminds/semver" + connections "github.com/sourcegraph/sourcegraph/internal/database/connections/live" + "github.com/sourcegraph/sourcegraph/internal/database/migration/schemas" + "github.com/sourcegraph/sourcegraph/internal/database/postgresdsn" + "github.com/sourcegraph/sourcegraph/internal/observation" "github.com/sourcegraph/sourcegraph/internal/version" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -47,3 +52,45 @@ func DetermineUpgradePolicy(currentVersion, targetVersion string) (downtime bool fmt.Println("✅ Standard upgrade policy selected.") return false, nil } + +// WIP this is a place holder for now and construncts DSNs from os.Getenv, +// ultimately we want to get the env vars from dbAuthVars as in frontend.go. +func getApplianceDSNs() (map[string]string, error) { + dsns, err := postgresdsn.DSNsBySchema(schemas.SchemaNames) + if err != nil { + return nil, err + } + return dsns, nil +} + +// checkConnection to one of our standard databases(pgsql, codeintel, codeinsights) +func checkConnection(obsvCtx *observation.Context, name, dsn string) error { + if name != "frontend" && name != "codeintel" && name != "codeinsights" { + return errors.Newf("invalid database name: %s", name) + } + + var connect func(*observation.Context, string, string) (*sql.DB, error) + switch name { + case "frontend": + connect = connections.RawNewFrontendDB + case "codeintel": + connect = connections.RawNewCodeIntelDB + case "codeinsights": + connect = connections.RawNewCodeInsightsDB + } + + fmt.Printf("Checking connection to %s database...\n", name) + + if db, err := connect(obsvCtx, dsn, "appliance"); err != nil { + return err + } else { + defer db.Close() + + if err := db.Ping(); err != nil { + return err + } + } + + fmt.Printf("✅ Connection to %s database successful.\n", name) + return nil +} diff --git a/internal/appliance/upgrades/util_test.go b/internal/appliance/upgrades/util_test.go index e450f2b5bde76..b862c72ad586f 100644 --- a/internal/appliance/upgrades/util_test.go +++ b/internal/appliance/upgrades/util_test.go @@ -1,7 +1,11 @@ package upgrades import ( + "os" "testing" + + "github.com/sourcegraph/sourcegraph/internal/database/dbtest" + "github.com/sourcegraph/sourcegraph/internal/observation" ) func TestDetermineUpgradePolicy(t *testing.T) { @@ -71,3 +75,158 @@ func TestDetermineUpgradePolicy(t *testing.T) { }) } } + +func TestCheckConnection_Ping(t *testing.T) { + tests := []struct { + name string + schema string + connection bool + }{ + { + name: "frontend single db connection", + schema: "frontend", + connection: true, + }, + { + name: "malformed dsn", + schema: "doombot", + connection: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db := dbtest.NewDB(t) + defer db.Close() + + var currentUser string + err := db.QueryRow("SELECT current_user").Scan(¤tUser) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + + url, err := dbtest.GetDSN() + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + password, _ := url.User.Password() + + var dbName string + err = db.QueryRow("SELECT current_database()").Scan(&dbName) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + + t.Setenv("CODEINTEL_PG_ALLOW_SINGLE_DB", "true") + t.Setenv("PGUSER", currentUser) + t.Setenv("PGPASSWORD", password) + t.Setenv("PGDATABASE", dbName) + t.Setenv("PGSSLMODE", "disable") + t.Setenv("PGTZ", "UTC") + + dsns, err := getApplianceDSNs() + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + + t.Log("test dsn: ", dsns[test.schema]) + err = checkConnection(&observation.TestContext, test.schema, dsns[test.schema]) + if err != nil && test.connection { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + }) + } +} + +// TODO: This should check that the connections with no envvars set remain as default, +// theres some odd handling of defaults setting current user in the postgresdsns package +// this may need further investigation. +func TestGetApplianceDSNs(t *testing.T) { + tests := []struct { + name string + schema string + envvars map[string]string + expectedDSN string + }{ + { + name: "default frontend", + schema: "frontend", + envvars: map[string]string{ + "PGHOST": "pgsql", + "PGPORT": "5432", + "PGUSER": "sg", + "PGPASSWORD": "sg", + "PGDATABASE": "sg", + "PGSSLMODE": "disable", + "PGTZ": "UTC", + }, + expectedDSN: "postgres://sg:sg@pgsql:5432/sg?sslmode=disable&timezone=UTC", + }, + { + name: "default codeintel", + schema: "codeintel", + envvars: map[string]string{ + "CODEINTEL_PGHOST": "codeintel-db", + "CODEINTEL_PGPORT": "5432", + "CODEINTEL_PGUSER": "sg", + "CODEINTEL_PGPASSWORD": "sg", + "CODEINTEL_PGDATABASE": "sg", + "CODEINTEL_PGSSLMODE": "disable", + "CODEINTEL_PGTZ": "UTC", + }, + expectedDSN: "postgres://sg:sg@codeintel-db:5432/sg?sslmode=disable&timezone=UTC", + }, + { + name: "default codeinsights", + schema: "codeinsights", + envvars: map[string]string{ + "CODEINSIGHTS_PGHOST": "codeinsights-db", + "CODEINSIGHTS_PGPORT": "5432", + "CODEINSIGHTS_PGUSER": "postgres", + "CODEINSIGHTS_PGPASSWORD": "password", + "CODEINSIGHTS_PGDATABASE": "postgres", + "CODEINSIGHTS_PGSSLMODE": "disable", + "CODEINSIGHTS_PGTZ": "UTC", + }, + expectedDSN: "postgres://postgres:password@codeinsights-db:5432/postgres?sslmode=disable&timezone=UTC", + }, + { + name: "unusual dsn", + schema: "frontend", + envvars: map[string]string{ + "PGHOST": "latveria", + "PGPORT": "6969", + "PGUSER": "doombot", + "PGPASSWORD": "allhaildoom", + "PGDATABASE": "postgres", + "PGSSLMODE": "disable", + "PGTZ": "UTC", + }, + expectedDSN: "postgres://doombot:allhaildoom@latveria:6969/postgres?sslmode=disable&timezone=UTC", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for k, v := range test.envvars { + os.Setenv(k, v) + } + + dsns, err := getApplianceDSNs() + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + t.Log("dsn: ", dsns) + + if dsns[test.schema] != test.expectedDSN { + t.Errorf("expected dsn %s, got %s", test.expectedDSN, dsns[test.schema]) + } + }) + } +} diff --git a/internal/appliance/versions.go b/internal/appliance/versions.go index 1c55874cc57b0..7cc86ebb13816 100644 --- a/internal/appliance/versions.go +++ b/internal/appliance/versions.go @@ -9,7 +9,7 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -// Return the subset of allVersions that are at most n minor revisions behind +// NMinorVersions returns the subset of allVersions that are at most n minor revisions behind // latestSupportedVersion. func NMinorVersions(allVersions []string, latestSupportedVersion string, n uint64) ([]string, error) { latestSupported, err := semver.NewVersion(latestSupportedVersion) @@ -17,18 +17,10 @@ func NMinorVersions(allVersions []string, latestSupportedVersion string, n uint6 return nil, errors.Wrap(err, "parsing latest supported version") } - versionsAndErrs := slices.Map(allVersions, func(versionStr string) semverAndError { - version, err := semver.NewVersion(versionStr) - return semverAndError{semver: version, err: errors.Wrapf(err, "error parsing semver: %s", versionStr)} - }) - versions := make([]*semver.Version, len(versionsAndErrs)) - for i, versionAndErr := range versionsAndErrs { - if versionAndErr.err != nil { - return nil, err - } - versions[i] = versionAndErr.semver + versions, err := ParseVersions(allVersions) + if err != nil { + return nil, errors.Wrap(err, "parsing versions") } - sort.Sort(semver.Collection(versions)) var nminor []*semver.Version for _, version := range versions { @@ -69,6 +61,22 @@ func NMinorVersions(allVersions []string, latestSupportedVersion string, n uint6 return slices.Map(nminor, func(semver *semver.Version) string { return semver.String() }), nil } +func ParseVersions(versionStrs []string) ([]*semver.Version, error) { + versionsAndErrs := slices.Map(versionStrs, func(versionStr string) semverAndError { + version, err := semver.NewVersion(versionStr) + return semverAndError{semver: version, err: errors.Wrapf(err, "error parsing semver: %s", versionStr)} + }) + versions := make([]*semver.Version, len(versionsAndErrs)) + for i, versionAndErr := range versionsAndErrs { + if versionAndErr.err != nil { + return nil, versionAndErr.err + } + versions[i] = versionAndErr.semver + } + sort.Sort(semver.Collection(versions)) + return versions, nil +} + func highestMinorInMajorSeries(versions []*semver.Version, major uint64) uint64 { // iterate backwards to start with the highest numbers for i := len(versions) - 1; i >= 0; i-- { diff --git a/internal/appliance/versions_test.go b/internal/appliance/versions_test.go index f1e0f7b841d2f..f4c84bfde3547 100644 --- a/internal/appliance/versions_test.go +++ b/internal/appliance/versions_test.go @@ -32,6 +32,7 @@ var allVersions = []string{ "4.0.0", "4.0.1", "4.1.0", + "4.1.1", } func TestNMinorVersions(t *testing.T) { diff --git a/internal/appliance/web/static/css/bootstrap.min.css b/internal/appliance/web/static/css/bootstrap.min.css deleted file mode 100644 index 46dc7faa2e923..0000000000000 --- a/internal/appliance/web/static/css/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.3.3 (https://getbootstrap.com/) - * Copyright 2011-2024 The Bootstrap Authors - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}.accordion-flush>.accordion-item>.accordion-collapse{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin:calc(-.5 * var(--bs-offcanvas-padding-y)) calc(-.5 * var(--bs-offcanvas-padding-x)) calc(-.5 * var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ diff --git a/internal/appliance/web/static/css/custom.css b/internal/appliance/web/static/css/custom.css deleted file mode 100644 index 929682e7aeb58..0000000000000 --- a/internal/appliance/web/static/css/custom.css +++ /dev/null @@ -1,17 +0,0 @@ -/* Define custom CSS variables */ -:root { - --bs-primary: #3498db; - --bs-secondary: #2ecc71; - --bs-success: #28a745; - --bs-info: #17a2b8; - --bs-warning: #ffc107; - --bs-danger: #dc3545; - --bs-light: #f8f9fa; - --bs-dark: #343a40; -} - -/* Override Bootstrap variables */ -body { - background-color: var(--bs-light); - color: var(--bs-dark); -} diff --git a/internal/appliance/web/static/img/favicon.png b/internal/appliance/web/static/img/favicon.png deleted file mode 100644 index 142da8747ed7b45d876c4d2726ed18093c8e82c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3473 zcmV;C4Q}#@P)EX>4Tx04R}tkv&MmKpe$iQ>8^J4i*$~$WWc^Qbim~6^me@v=v%)FuC*#nlvOS zE{=k0!NHHks)LKOt`4q(Aou~|>f)s6A|?JWDYS_3;J6>}?mh0_0YaNsHm-ExlVH&Nh~3SG(^Z~po$tSL}}GXF_EGDgol63@u$fpldB3w zjs?`ALUR1zfAG6ovosxZlR^og|HZaHCV;>$&~DoH_pxoaPXPZjaHVzq^%gMqNqV!Z zMUH@>ZQ$a%tI2!7A(Ki)<;agx}&F!tTkJASrM_n!700)P_ zWSO$pJ>DJc?d{()o&J6Rs|Rwu_D*E<00006VoOIv0RI600RN!9r;`8x010qNS#tmY z4c7nw4c7reD4Tcy000McNliru=L`t|1uaL#iy;6202y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{01IhJL_t(|+U=cta8%_Lz<=lNCcAkU9s**eDkzT{2a!ia z9+nU=0TY9Ph||_OIy#QFKBhWVYb)qrnzx(^ncfRwzPJ|O6I(O!Hd{z<4g+O1R z8I_$Flv7f%UO?%kB0UOn{^b{9MgIyixtqs!A$qHnRBk#2v^$vda`F*MfRq>>w-C8o zN-7qe0{X3%nKJ>4EkIJ7C9juIi`UcVYKy$rOUZ4IX6It8?hF9t?QS_qLJ0U6rS4L}eqf<)e5P3vQ)5qKgS ziF&WgL$iIiVdXhdkyW&0M@f1MxE07a&SU2Qi;mE?xs;Z(y$E!d1CIC-oinYk9Sy5# z%@<@{tUX52OBAVC#gQ|-2J~e|e0`E{MxupR(tHW96iAM<=y1XRw8z3GEq5qd$W07p zGq4>8bnApPyVnj1U=R6-#Wk@XI!8lrQv zGvf%0`b05a0KRL#E#)#V`A`SDy^$9q%#_Jo8wA9>*xhqL4V>|#mXUj$!2MQ`U$|rqPkIli_S*y;all;izG<0} z`M{rCs`;7%QZ{-HsDwgoz)KFO8lgoqhJ6LIB}dU`UHX;}gtqJBo6;RmHmINU@Oc=d zqrNuiw}s{1(Hc)mM<2y$-$DNozc zA#86v2h)59SGGfRKPsgdMu>6!wjiQ zw@vd_T-#fcp$NC5*2}z7zmM8Ozn9yWjxh~mYrLA?eC8a1XveogFnl!<|DeTGvW2`)cHfbJkuP_RP`2zO6^e-?yEjw`$1S z|0+!f#!$QEm#F$&=;pj@5uIE}`lrt7tqUJSISY?TuszD?>SQKWBr|cNkJO`%TIgp2 zQwj*~b-tl$!^?oVcvzC#gfZ1VCRO^$+T$rjuu#h6w|@(y7X_OC`n-3QuPb~N_Iv}U4Oz_IPwlgV-qf|Q}w z8jcQFOLqL{LE{J7S*i}C;1A9~q!=jV_-8D8qtpXssD?-zP)??_xev6nY}g+pEqx|p zgr5FcBX`hN)Lu24|l{a1Vnl zCb|8=&Mu)xt|peD8Brn|-yLOMyfxdTYI>A|=UOL86vke}n$5UD@AL@J5;QqTWDPL0 z^S>V$YEhLFWox#{z7b|`$9e#tPGs!Bk6Z=105!%FS*lRn-E*IvsZ@VI%H|pE>=|vL z(ko5%1ty%H?`qXo)A~3N*XCFF4CR$cQEH}|Y@Zax^z|a9N|v|>Gx7lFgLZU$-e1`^ zElSn&C_BfS-iIqZ=?c(QwA>`fVkdPT$xv#hn!Gl>k(!AHN0S3yL|W%~o0rmB0OI)Z z!sLi>;m4nFpWMZrudihEp$`z0_xcQ?-RJX=kviJVnp)MGMa}(F{*IjlGWG6Frs8lS-`+EUX80~ z2PkeFg=|8Mvuzy`D@9YGZMCAK+-UI3F^dy~384KL=$im+MZ^ML>G)aLv42(!GT%8v z(4RBD|KWs3;-}CgC9+J>Np8R#nfWhOL5%l76L`Z#WSj~p8&>!Jm)grE~)G3lLK^ryQ{j)IcILfPcqb^4LX;MZhBV?hVk8 zF$%xoCz{kiR-gBdw_ECu4`$0Wg~wfj`G!H@s%)h77z(Q#HGXAVt0lP!0qrcqhev?d zVr#(|r36h;WC`HLJnucl%*{s{zw&R>SnLwoj`Ug`y^CHgy#ho*x&4N{U1;Mw!eUx@669k&)IiL#T9Ujfw4Jaxg_4qeJ zEX$cp5NNI!E!4?{2DQ+e@(FfpfgNcs-*n)8ll6zQGCTuRkL~ibIB1=B7n1T!j1N1J z2_lI|d$wnQ3ZkI99WhOKw1D6r;wa!yMjb2HQ(ay#R@sNCORgkvPh5qLByR$aIzV)m zr+`{S|AD;}7i&Kuc)i4VVP4j*jet0j3F2IxpEJTUKnqFxNZ|g&e5OLvOPa3I_Jfy*unyOKb=q)z2GJIt8=~RrwvzmSE{xmQ}uZ3TS7Zp^BX-3lk`M1k3m1 zp7=dYy~{!YS+@RR&`07Zau{TmEdS3QPfb0&WvP|-A$sNik4>cvQ^rcDVsnqxjb1Fe zV1dEzO?g-{12Hl|{XhdEyAYm}=PGvhbe;YSL$CH4ZDW^&00000NkvXXu0mjfWX)!i diff --git a/internal/appliance/web/static/script/bootstrap.bundle.min.js b/internal/appliance/web/static/script/bootstrap.bundle.min.js deleted file mode 100644 index d705b8dac58e6..0000000000000 --- a/internal/appliance/web/static/script/bootstrap.bundle.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v5.3.3 (https://getbootstrap.com/) - * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>n(t))).join(","):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",jt="collapsing",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",js="Home",Ms="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); -//# sourceMappingURL=bootstrap.bundle.min.js.map diff --git a/internal/appliance/web/static/script/htmx.min.js b/internal/appliance/web/static/script/htmx.min.js deleted file mode 100644 index 2172f7ab79e2d..0000000000000 --- a/internal/appliance/web/static/script/htmx.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s(""+n+"
",1);case"col":return s(""+n+"
",2);case"tr":return s(""+n+"
",2);case"td":case"th":return s(""+n+"
",3);case"script":case"style":return s("
"+n+"
",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;if(!t){return false}for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var B=p.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}p=(B[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); diff --git a/internal/appliance/web/template/error.gohtml b/internal/appliance/web/template/error.gohtml deleted file mode 100644 index 1c1c26f025b56..0000000000000 --- a/internal/appliance/web/template/error.gohtml +++ /dev/null @@ -1,8 +0,0 @@ -{{ template "layout.gohtml" }} - -{{ define "title" }}Something went wrong.{{ end }} - -{{- define "content" }} -

Something went wrong.

-

{{ .Msg }}

-{{- end }} diff --git a/internal/appliance/web/template/landing.gohtml b/internal/appliance/web/template/landing.gohtml deleted file mode 100644 index 6e3533e331234..0000000000000 --- a/internal/appliance/web/template/landing.gohtml +++ /dev/null @@ -1,22 +0,0 @@ -{{ template "layout.gohtml" }} - -{{ define "title" }}Sourcegraph Maintenance{{ end }} - -{{- define "content" }} -

Sourcegraph maintenance

-{{- if ne .Flash "" }} - -{{- end }} - -
-
-
- - -
-
- -
-{{- end }} diff --git a/internal/appliance/web/template/layout.gohtml b/internal/appliance/web/template/layout.gohtml deleted file mode 100644 index c88c0bca31e70..0000000000000 --- a/internal/appliance/web/template/layout.gohtml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - {{ block "title" . }}{{ end }} - - - -{{ block "content" . }}{{ end }} - - diff --git a/internal/appliance/web/template/setup.gohtml b/internal/appliance/web/template/setup.gohtml deleted file mode 100644 index 1dd6fb0601e7e..0000000000000 --- a/internal/appliance/web/template/setup.gohtml +++ /dev/null @@ -1,149 +0,0 @@ -{{ template "layout.gohtml" }} - -{{ define "title" }}Sourcegraph Appliance - Setup{{ end }} - -{{- define "content" }} -

Sourcegraph Appliance Setup

- -
-
-
- -

Choose the Sourcegraph version that you would like to install.

-
-
- -
-
- -
- -
-
- -

Would you like to use an external database?

-
-
-
- - -
- - -
-
-

- -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-

- -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-

- -

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- -
-
-
-
- -

Would you like to run in development mode (No resource requests/limits)?

-
-
-
- - -
-
-
- -
-{{- end }} diff --git a/internal/conf/computed.go b/internal/conf/computed.go index 746f65f448e1c..e5da6a47cb98f 100644 --- a/internal/conf/computed.go +++ b/internal/conf/computed.go @@ -3,7 +3,9 @@ package conf import ( "encoding/hex" "log" //nolint:logging // TODO move all logging to sourcegraph/log + "os" "slices" + "strconv" "strings" "time" @@ -441,6 +443,13 @@ func AuthPrimaryLoginProvidersCount() int { return c } +func IsApplianceManaged() bool { + if v, _ := strconv.ParseBool(os.Getenv("APPLIANCE_MANAGED")); v { + return v + } + return false +} + // SearchSymbolsParallelism returns 20, or the site config // "debug.search.symbolsParallelism" value if configured. func SearchSymbolsParallelism() int { diff --git a/internal/database/postgresdsn/postgresdsn.go b/internal/database/postgresdsn/postgresdsn.go index f9c24a9b007f1..9d508a5983986 100644 --- a/internal/database/postgresdsn/postgresdsn.go +++ b/internal/database/postgresdsn/postgresdsn.go @@ -6,6 +6,7 @@ import ( "strings" ) +// New parses Sourcegraph database service environment variables to construct a Postgres DSN func New(prefix, currentUser string, getenv func(string) string) string { if prefix == "frontend" { prefix = "" diff --git a/internal/releaseregistry/client.go b/internal/releaseregistry/client.go index 56d630418fe71..d87e73945df28 100644 --- a/internal/releaseregistry/client.go +++ b/internal/releaseregistry/client.go @@ -21,6 +21,15 @@ type ReleaseInfo struct { IsDevelopment bool `json:"is_development"` } +// If you're wondering why we define an interface here, Java-style, instead of +// using the more Go-ish idiom of encouraging consumers define interfaces, it's +// because there are a couple of packages we want to use mocks for this +// interface in, and its simpler to generate them in one place and import them +// everywhere. +type ReleaseRegistryClient interface { + ListVersions(ctx context.Context, product string) ([]ReleaseInfo, error) +} + type Client struct { endpoint string client http.Client diff --git a/internal/releaseregistry/mocks/BUILD.bazel b/internal/releaseregistry/mocks/BUILD.bazel new file mode 100644 index 0000000000000..30fd259c69d7b --- /dev/null +++ b/internal/releaseregistry/mocks/BUILD.bazel @@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_mockgen.bzl", "go_mockgen") + +go_library( + name = "mocks", + srcs = ["mocks.go"], + importpath = "github.com/sourcegraph/sourcegraph/internal/releaseregistry/mocks", + visibility = ["//:__subpackages__"], + deps = ["//internal/releaseregistry"], +) + +go_mockgen( + name = "generate_mocks", + out = "mocks.go", + manifests = [ + "//:mockgen.yaml", + "//:mockgen.test.yaml", + "//:mockgen.temp.yaml", + ], + deps = ["//internal/releaseregistry"], +) diff --git a/internal/releaseregistry/mocks/mocks.go b/internal/releaseregistry/mocks/mocks.go new file mode 100644 index 0000000000000..4e6be536553b3 --- /dev/null +++ b/internal/releaseregistry/mocks/mocks.go @@ -0,0 +1,172 @@ +// Code generated by go-mockgen 1.3.7; DO NOT EDIT. +// +// This file was generated by running `sg generate` (or `go-mockgen`) at the root of +// this repository. To add additional mocks to this or another package, add a new entry +// to the mockgen.yaml file in the root of this repository. + +package mocks + +import ( + "context" + "sync" + + releaseregistry "github.com/sourcegraph/sourcegraph/internal/releaseregistry" +) + +// MockReleaseRegistryClient is a mock implementation of the +// ReleaseRegistryClient interface (from the package +// github.com/sourcegraph/sourcegraph/internal/releaseregistry) used for +// unit testing. +type MockReleaseRegistryClient struct { + // ListVersionsFunc is an instance of a mock function object controlling + // the behavior of the method ListVersions. + ListVersionsFunc *ReleaseRegistryClientListVersionsFunc +} + +// NewMockReleaseRegistryClient creates a new mock of the +// ReleaseRegistryClient interface. All methods return zero values for all +// results, unless overwritten. +func NewMockReleaseRegistryClient() *MockReleaseRegistryClient { + return &MockReleaseRegistryClient{ + ListVersionsFunc: &ReleaseRegistryClientListVersionsFunc{ + defaultHook: func(context.Context, string) (r0 []releaseregistry.ReleaseInfo, r1 error) { + return + }, + }, + } +} + +// NewStrictMockReleaseRegistryClient creates a new mock of the +// ReleaseRegistryClient interface. All methods panic on invocation, unless +// overwritten. +func NewStrictMockReleaseRegistryClient() *MockReleaseRegistryClient { + return &MockReleaseRegistryClient{ + ListVersionsFunc: &ReleaseRegistryClientListVersionsFunc{ + defaultHook: func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) { + panic("unexpected invocation of MockReleaseRegistryClient.ListVersions") + }, + }, + } +} + +// NewMockReleaseRegistryClientFrom creates a new mock of the +// MockReleaseRegistryClient interface. All methods delegate to the given +// implementation, unless overwritten. +func NewMockReleaseRegistryClientFrom(i releaseregistry.ReleaseRegistryClient) *MockReleaseRegistryClient { + return &MockReleaseRegistryClient{ + ListVersionsFunc: &ReleaseRegistryClientListVersionsFunc{ + defaultHook: i.ListVersions, + }, + } +} + +// ReleaseRegistryClientListVersionsFunc describes the behavior when the +// ListVersions method of the parent MockReleaseRegistryClient instance is +// invoked. +type ReleaseRegistryClientListVersionsFunc struct { + defaultHook func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) + hooks []func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) + history []ReleaseRegistryClientListVersionsFuncCall + mutex sync.Mutex +} + +// ListVersions delegates to the next hook function in the queue and stores +// the parameter and result values of this invocation. +func (m *MockReleaseRegistryClient) ListVersions(v0 context.Context, v1 string) ([]releaseregistry.ReleaseInfo, error) { + r0, r1 := m.ListVersionsFunc.nextHook()(v0, v1) + m.ListVersionsFunc.appendCall(ReleaseRegistryClientListVersionsFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the ListVersions method +// of the parent MockReleaseRegistryClient instance is invoked and the hook +// queue is empty. +func (f *ReleaseRegistryClientListVersionsFunc) SetDefaultHook(hook func(context.Context, string) ([]releaseregistry.ReleaseInfo, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// ListVersions method of the parent MockReleaseRegistryClient instance +// invokes the hook at the front of the queue and discards it. After the +// queue is empty, the default hook function is invoked for any future +// action. +func (f *ReleaseRegistryClientListVersionsFunc) PushHook(hook func(context.Context, string) ([]releaseregistry.ReleaseInfo, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *ReleaseRegistryClientListVersionsFunc) SetDefaultReturn(r0 []releaseregistry.ReleaseInfo, r1 error) { + f.SetDefaultHook(func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *ReleaseRegistryClientListVersionsFunc) PushReturn(r0 []releaseregistry.ReleaseInfo, r1 error) { + f.PushHook(func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) { + return r0, r1 + }) +} + +func (f *ReleaseRegistryClientListVersionsFunc) nextHook() func(context.Context, string) ([]releaseregistry.ReleaseInfo, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *ReleaseRegistryClientListVersionsFunc) appendCall(r0 ReleaseRegistryClientListVersionsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of ReleaseRegistryClientListVersionsFuncCall +// objects describing the invocations of this function. +func (f *ReleaseRegistryClientListVersionsFunc) History() []ReleaseRegistryClientListVersionsFuncCall { + f.mutex.Lock() + history := make([]ReleaseRegistryClientListVersionsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// ReleaseRegistryClientListVersionsFuncCall is an object that describes an +// invocation of method ListVersions on an instance of +// MockReleaseRegistryClient. +type ReleaseRegistryClientListVersionsFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 string + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 []releaseregistry.ReleaseInfo + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c ReleaseRegistryClientListVersionsFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c ReleaseRegistryClientListVersionsFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} diff --git a/mockgen.test.yaml b/mockgen.test.yaml index 5c82cba4dbc26..3d1af01552756 100644 --- a/mockgen.test.yaml +++ b/mockgen.test.yaml @@ -447,3 +447,8 @@ package: analytics interfaces: - secretStore +- filename: internal/releaseregistry/mocks/mocks.go + path: github.com/sourcegraph/sourcegraph/internal/releaseregistry + package: mocks + interfaces: + - ReleaseRegistryClient diff --git a/wolfi-images/appliance-frontend.lock.json b/wolfi-images/appliance-frontend.lock.json new file mode 100755 index 0000000000000..147eceb12c77f --- /dev/null +++ b/wolfi-images/appliance-frontend.lock.json @@ -0,0 +1,885 @@ +{ + "configHash": "cea5c8cd77ec03d251d31528709b406d486800dd70cff0f27aa6a0c2078ee4bd", + "contents": { + "keyring": [ + { + "name": "packages.wolfi.dev/os/wolfi-signing.rsa.pub", + "url": "https://packages.wolfi.dev/os/wolfi-signing.rsa.pub" + }, + { + "name": "packages.sgdev.org/sourcegraph-melange-prod.rsa.pub", + "url": "https://packages.sgdev.org/sourcegraph-melange-prod.rsa.pub" + } + ], + "packages": [ + { + "architecture": "x86_64", + "checksum": "Q1WR9MGKU+cJh6dJFEXnr3zU1qsMM=", + "control": { + "checksum": "sha1-WR9MGKU+cJh6dJFEXnr3zU1qsMM=", + "range": "bytes=705-1034" + }, + "data": { + "checksum": "sha256-EkOhtSivoLErXsK2J9ri39thkwb2kJGYHVou4n6dMKA=", + "range": "bytes=1035-254654" + }, + "name": "ca-certificates-bundle", + "signature": { + "checksum": "sha1-WL9vKv1nPIeaUQI4O0DiY9RdSHE=", + "range": "bytes=0-704" + }, + "url": "https://packages.wolfi.dev/os/x86_64/ca-certificates-bundle-20240705-r0.apk", + "version": "20240705-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1KmG1L67f+/TnVGcnIcLfWRF/K2g=", + "control": { + "checksum": "sha1-KmG1L67f+/TnVGcnIcLfWRF/K2g=", + "range": "bytes=702-1051" + }, + "data": { + "checksum": "sha256-2YIxYc/Quit+Kri2qS8p6XvL7taAmOYJU/1Z8RNQ+88=", + "range": "bytes=1052-122509" + }, + "name": "wolfi-baselayout", + "signature": { + "checksum": "sha1-VVyPrknEtqCPym5lfhVEuIhfkKg=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/wolfi-baselayout-20230201-r13.apk", + "version": "20230201-r13" + }, + { + "architecture": "x86_64", + "checksum": "Q1ASyaCJL2Zyz4uAKpPPlx1Sd40yo=", + "control": { + "checksum": "sha1-ASyaCJL2Zyz4uAKpPPlx1Sd40yo=", + "range": "bytes=706-1101" + }, + "data": { + "checksum": "sha256-KoaUrlV3QZ4ATGvKgOmZ1C8Rb/tHK3zEiCTGDrxxc/I=", + "range": "bytes=1102-272818" + }, + "name": "ld-linux", + "signature": { + "checksum": "sha1-DBrLE0fWPMyB5/0kAtNlIK6viCU=", + "range": "bytes=0-705" + }, + "url": "https://packages.wolfi.dev/os/x86_64/ld-linux-2.39-r7.apk", + "version": "2.39-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q10iHLABsXtuH2JyExQ2IIxH1NUCI=", + "control": { + "checksum": "sha1-0iHLABsXtuH2JyExQ2IIxH1NUCI=", + "range": "bytes=698-1041" + }, + "data": { + "checksum": "sha256-A3RF8XLSIePkapCnvEridXa2N7nheKNT1kTKDqZlGdI=", + "range": "bytes=1042-408849" + }, + "name": "glibc-locale-posix", + "signature": { + "checksum": "sha1-QqrJFnJmyPeT66hUk3O7NxaWY8I=", + "range": "bytes=0-697" + }, + "url": "https://packages.wolfi.dev/os/x86_64/glibc-locale-posix-2.39-r7.apk", + "version": "2.39-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q1JYf1zI1zVi/S9sMI32guk6ptuRQ=", + "control": { + "checksum": "sha1-JYf1zI1zVi/S9sMI32guk6ptuRQ=", + "range": "bytes=701-1308" + }, + "data": { + "checksum": "sha256-ioC8korS2nJpJvoS6He+mKkDsgVLcMxiEuYLsAB9M7U=", + "range": "bytes=1309-5959239" + }, + "name": "glibc", + "signature": { + "checksum": "sha1-3TP2+cHjCEBrIFluCdTmr2oJA1s=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/glibc-2.39-r7.apk", + "version": "2.39-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q1WAlPh7UNBV+RHfBixVB5P9F9Mp8=", + "control": { + "checksum": "sha1-WAlPh7UNBV+RHfBixVB5P9F9Mp8=", + "range": "bytes=700-1074" + }, + "data": { + "checksum": "sha256-i6uEFvUEbJ4YzgNSV58aObIdU608mjbCVPwyF57ZeQQ=", + "range": "bytes=1075-81593" + }, + "name": "protobuf-c", + "signature": { + "checksum": "sha1-47zwDXmhcWiVtTYCjq4sFld8wHQ=", + "range": "bytes=0-699" + }, + "url": "https://packages.wolfi.dev/os/x86_64/protobuf-c-1.5.0-r6.apk", + "version": "1.5.0-r6" + }, + { + "architecture": "x86_64", + "checksum": "Q144JvgxJGCpXrE6GLHwSuuttlkxs=", + "control": { + "checksum": "sha1-44JvgxJGCpXrE6GLHwSuuttlkxs=", + "range": "bytes=693-1040" + }, + "data": { + "checksum": "sha256-TCxwGLHrXXmv5fE5TXJNh4qwDu327dn4U1IbcL6v8YA=", + "range": "bytes=1041-26390" + }, + "name": "krb5-conf", + "signature": { + "checksum": "sha1-5ExTpBXZ6EvHv0oyahhZlvhqCfs=", + "range": "bytes=0-692" + }, + "url": "https://packages.wolfi.dev/os/x86_64/krb5-conf-1.0-r3.apk", + "version": "1.0-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1v0A2vQM5k8rQY/GpIq6xI4T0YmA=", + "control": { + "checksum": "sha1-v0A2vQM5k8rQY/GpIq6xI4T0YmA=", + "range": "bytes=701-1064" + }, + "data": { + "checksum": "sha256-uAOrkGDDc/yqt6VjIrlTEALBJG4BCI4njqZw5jQqF0o=", + "range": "bytes=1065-56984" + }, + "name": "keyutils-libs", + "signature": { + "checksum": "sha1-32uYMH/zGBr9Jo3hiqnThlcqBcE=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/keyutils-libs-1.6.3-r3.apk", + "version": "1.6.3-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1Onuac6Y+IWA7a4b8FvqX1erWDDw=", + "control": { + "checksum": "sha1-Onuac6Y+IWA7a4b8FvqX1erWDDw=", + "range": "bytes=693-1048" + }, + "data": { + "checksum": "sha256-KLtDAs3iU0oyZ/FysePG/J+1H1ofDxHXIY7xvtZhtMc=", + "range": "bytes=1049-61503" + }, + "name": "libverto", + "signature": { + "checksum": "sha1-/IXL9S1O+MwaW7KsSzz1ywEqKFQ=", + "range": "bytes=0-692" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libverto-0.3.2-r4.apk", + "version": "0.3.2-r4" + }, + { + "architecture": "x86_64", + "checksum": "Q1LiIUdkSXe1wl+7fXVKY1Vt9gWdE=", + "control": { + "checksum": "sha1-LiIUdkSXe1wl+7fXVKY1Vt9gWdE=", + "range": "bytes=704-1142" + }, + "data": { + "checksum": "sha256-q1+BZH2cUxNatRng6hhRdCAglKhEPRliqjyF0J7VGxw=", + "range": "bytes=1143-51521" + }, + "name": "libcom_err", + "signature": { + "checksum": "sha1-W38fA6agxV2HLxvbpE9Oe89oM/M=", + "range": "bytes=0-703" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libcom_err-1.47.1-r0.apk", + "version": "1.47.1-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1a5wJzZI7k9o4F8hHFpfuqAUIH5Q=", + "control": { + "checksum": "sha1-a5wJzZI7k9o4F8hHFpfuqAUIH5Q=", + "range": "bytes=710-1075" + }, + "data": { + "checksum": "sha256-2p1UC4F4Odfxup/J/nyzujYRGaxGVjBOBWoc0aajClI=", + "range": "bytes=1076-5903889" + }, + "name": "libcrypto3", + "signature": { + "checksum": "sha1-SOFoklqXtH38i1PcI3G/cPdRFtY=", + "range": "bytes=0-709" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libcrypto3-3.3.1-r4.apk", + "version": "3.3.1-r4" + }, + { + "architecture": "x86_64", + "checksum": "Q1gb2VyLOsGV7xUiHBT0Ypt64rw6Y=", + "control": { + "checksum": "sha1-gb2VyLOsGV7xUiHBT0Ypt64rw6Y=", + "range": "bytes=702-1071" + }, + "data": { + "checksum": "sha256-h2tz/aFB2n8SFdBFde5foQm1ru+8JUcGp+trUeA2ZlQ=", + "range": "bytes=1072-1188954" + }, + "name": "libssl3", + "signature": { + "checksum": "sha1-w6U2Pb1Y90u1iBFc/sHg2xn6Qmo=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libssl3-3.3.1-r4.apk", + "version": "3.3.1-r4" + }, + { + "architecture": "x86_64", + "checksum": "Q1p04MrHQPrXOzuV/uF290zjnltqI=", + "control": { + "checksum": "sha1-p04MrHQPrXOzuV/uF290zjnltqI=", + "range": "bytes=704-1219" + }, + "data": { + "checksum": "sha256-TsXeqhUGykAhjQUEAlMUSzDQBcWDkkDvReoeHCL1F6c=", + "range": "bytes=1220-2555814" + }, + "name": "krb5-libs", + "signature": { + "checksum": "sha1-cPyzS38QHuNlOCQb8IPlsl2PLHQ=", + "range": "bytes=0-703" + }, + "url": "https://packages.wolfi.dev/os/x86_64/krb5-libs-1.21.3-r0.apk", + "version": "1.21.3-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1BMcnbxIFejKxlfYLJaX4uo/+X0M=", + "control": { + "checksum": "sha1-BMcnbxIFejKxlfYLJaX4uo/+X0M=", + "range": "bytes=699-1059" + }, + "data": { + "checksum": "sha256-Rp339LV5T4+zr98USK12eoyKGF84NviKlq3jh1AvbQQ=", + "range": "bytes=1060-96440" + }, + "name": "fstrm", + "signature": { + "checksum": "sha1-tOY0x5olyX6V9uRtlgxtf4WEhR0=", + "range": "bytes=0-698" + }, + "url": "https://packages.wolfi.dev/os/x86_64/fstrm-0.6.1-r1.apk", + "version": "0.6.1-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q17uprcuQ2VFudMcWAwtP7miJmLYs=", + "control": { + "checksum": "sha1-7uprcuQ2VFudMcWAwtP7miJmLYs=", + "range": "bytes=695-1067" + }, + "data": { + "checksum": "sha256-unKdPbvO2VHZ5w1t1l8mOpQuNdUGwLhofEO4weN2Mig=", + "range": "bytes=1068-280973" + }, + "name": "libuv", + "signature": { + "checksum": "sha1-4lKldnTO71WHMZ4tbANz6i2/Ed0=", + "range": "bytes=0-694" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libuv-1.48.0-r3.apk", + "version": "1.48.0-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1CN6zJbOPznWUfTrb/41KT7E6dm8=", + "control": { + "checksum": "sha1-CN6zJbOPznWUfTrb/41KT7E6dm8=", + "range": "bytes=699-1083" + }, + "data": { + "checksum": "sha256-ybzFzoQh1yNTcd3cjt54nYsPlAxAcMUcVMxEv1JRnpQ=", + "range": "bytes=1084-155375" + }, + "name": "zlib", + "signature": { + "checksum": "sha1-CPXJcCjinAXP+H8Z5tkko8ayG6E=", + "range": "bytes=0-698" + }, + "url": "https://packages.wolfi.dev/os/x86_64/zlib-1.3.1-r3.apk", + "version": "1.3.1-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1rgbci8SRSjW7EB0NSh6ZUPJLndM=", + "control": { + "checksum": "sha1-rgbci8SRSjW7EB0NSh6ZUPJLndM=", + "range": "bytes=702-1060" + }, + "data": { + "checksum": "sha256-tf4xbo+NP+ssiaZtC1kOKehffAZ3+d17bhgfU3/30+g=", + "range": "bytes=1061-256582" + }, + "name": "libnghttp2-14", + "signature": { + "checksum": "sha1-KrsJYTyUsll+EUhHekbkdZFzeig=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libnghttp2-14-1.62.1-r1.apk", + "version": "1.62.1-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q1A7oFO5lfWAO5koSxmIIusVW9920=", + "control": { + "checksum": "sha1-A7oFO5lfWAO5koSxmIIusVW9920=", + "range": "bytes=703-1094" + }, + "data": { + "checksum": "sha256-67UgNf9VPGz3z/GH4BT5oncUbjLkjqvEvxC/2JXyv2E=", + "range": "bytes=1095-113621" + }, + "name": "libev", + "signature": { + "checksum": "sha1-6ZgFt6PhwmoYFAA69/6inGbdJtU=", + "range": "bytes=0-702" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libev-4.33-r6.apk", + "version": "4.33-r6" + }, + { + "architecture": "x86_64", + "checksum": "Q1DLAp18ebMscosnQ4TfTivIzgtCY=", + "control": { + "checksum": "sha1-DLAp18ebMscosnQ4TfTivIzgtCY=", + "range": "bytes=698-1079" + }, + "data": { + "checksum": "sha256-eiBdqqECWnULWGe/Gnd6iv/7h56A4xR2qw+5toY5Gaw=", + "range": "bytes=1080-185559" + }, + "name": "libgcc", + "signature": { + "checksum": "sha1-qVj9laNeBIwRxP+8ehB+fXftJrw=", + "range": "bytes=0-697" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libgcc-13.3.0-r0.apk", + "version": "13.3.0-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1p9aaHhpB2Ud0URrcIrVRAujMw+o=", + "control": { + "checksum": "sha1-p9aaHhpB2Ud0URrcIrVRAujMw+o=", + "range": "bytes=696-1097" + }, + "data": { + "checksum": "sha256-rGMnyLiGEDCEVgqA1oXMf28QHuWHAaPzYrLeDzQ3WOU=", + "range": "bytes=1098-3155519" + }, + "name": "libstdc++", + "signature": { + "checksum": "sha1-lge2XoAsgvp8U6ZBgT52N2xNQAw=", + "range": "bytes=0-695" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libstdc++-13.3.0-r0.apk", + "version": "13.3.0-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1DUpLMidX5HbXUAFV8y3Imyt+Ank=", + "control": { + "checksum": "sha1-DUpLMidX5HbXUAFV8y3Imyt+Ank=", + "range": "bytes=698-1063" + }, + "data": { + "checksum": "sha256-ZUDWWOBnxl5xi3b5HQCHaiYqKDSrnVpJQgavWpn1duc=", + "range": "bytes=1064-255810" + }, + "name": "c-ares", + "signature": { + "checksum": "sha1-0AXk7b/B1baNADDg/NSocbW//pw=", + "range": "bytes=0-697" + }, + "url": "https://packages.wolfi.dev/os/x86_64/c-ares-1.32.1-r0.apk", + "version": "1.32.1-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1g2RQKAvNjlRPxWjinIRveaq7IlY=", + "control": { + "checksum": "sha1-g2RQKAvNjlRPxWjinIRveaq7IlY=", + "range": "bytes=695-1154" + }, + "data": { + "checksum": "sha256-UsQFfOTXCZBan0mxDvR+ts0NKcbv1lNijxnRxjsmZhA=", + "range": "bytes=1155-2566233" + }, + "name": "nghttp2", + "signature": { + "checksum": "sha1-SUvMhz1cc5Or5Qh4fUEvPCp0NAw=", + "range": "bytes=0-694" + }, + "url": "https://packages.wolfi.dev/os/x86_64/nghttp2-1.62.1-r1.apk", + "version": "1.62.1-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q1sTMGOgdbZyDKxlRAByisPMHTbpY=", + "control": { + "checksum": "sha1-sTMGOgdbZyDKxlRAByisPMHTbpY=", + "range": "bytes=703-1060" + }, + "data": { + "checksum": "sha256-bUy0h/zxpq7iP6vrQRMftZ9nevkHTB87hSX17wz4lmM=", + "range": "bytes=1061-631449" + }, + "name": "nghttp2-dev", + "signature": { + "checksum": "sha1-fQ6aDq3Io/gdJ5s5t3M+FwED1Y0=", + "range": "bytes=0-702" + }, + "url": "https://packages.wolfi.dev/os/x86_64/nghttp2-dev-1.62.1-r1.apk", + "version": "1.62.1-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q1rD3dEbbN8s5QAIQK1I8uF1O+oSo=", + "control": { + "checksum": "sha1-rD3dEbbN8s5QAIQK1I8uF1O+oSo=", + "range": "bytes=701-1154" + }, + "data": { + "checksum": "sha256-41W2IRSRALQijYojLaqAiSSN1uO7twatGn8jqEuCZUg=", + "range": "bytes=1155-1439872" + }, + "name": "xz", + "signature": { + "checksum": "sha1-lozWkVrKvSrd1SoM7Zv08YYG2pA=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/xz-5.6.2-r0.apk", + "version": "5.6.2-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1ORn/60iAvLcAgApXYoZzM6o0XSs=", + "control": { + "checksum": "sha1-ORn/60iAvLcAgApXYoZzM6o0XSs=", + "range": "bytes=703-1081" + }, + "data": { + "checksum": "sha256-Ya2Dn1VNYB21ZBD/jBCSx3SxZuJloJET5xkjX0s3Vt8=", + "range": "bytes=1082-4272708" + }, + "name": "libxml2", + "signature": { + "checksum": "sha1-s5jaTwJdla4k4KK0KEk7AYbgdqs=", + "range": "bytes=0-702" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libxml2-2.13.2-r0.apk", + "version": "2.13.2-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1wbnfumlWEiJnVUsJBRcQjFD3U6I=", + "control": { + "checksum": "sha1-wbnfumlWEiJnVUsJBRcQjFD3U6I=", + "range": "bytes=699-1222" + }, + "data": { + "checksum": "sha256-IXMNbJjj3kTjkWVBIHVRfG3zJQY2r5Eslgh/3TpMSsE=", + "range": "bytes=1223-3886778" + }, + "name": "bind-libs", + "signature": { + "checksum": "sha1-JNN15IPpJ2hMsO6hFVpDYPvvX5g=", + "range": "bytes=0-698" + }, + "url": "https://packages.wolfi.dev/os/x86_64/bind-libs-9.18.27-r1.apk", + "version": "9.18.27-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q1kXUOoNa2KXixxAASo1W/im1qZ7s=", + "control": { + "checksum": "sha1-kXUOoNa2KXixxAASo1W/im1qZ7s=", + "range": "bytes=702-1215" + }, + "data": { + "checksum": "sha256-HRADvfRXfYsgEjUMGM4Hd7prpIv+YWILVfovbGOkEME=", + "range": "bytes=1216-879975" + }, + "name": "bind-tools", + "signature": { + "checksum": "sha1-8WUTMUaJTaX0T9FS/c+GddL7ttY=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/bind-tools-9.18.27-r1.apk", + "version": "9.18.27-r1" + }, + { + "architecture": "x86_64", + "checksum": "Q1IbvTzcXUyY3M4HSurKCl4pqkI2o=", + "control": { + "checksum": "sha1-IbvTzcXUyY3M4HSurKCl4pqkI2o=", + "range": "bytes=697-1092" + }, + "data": { + "checksum": "sha256-/rIlvPr9j1TCCfCup07Nn1XGUs3HVoAbLFHd12zyzII=", + "range": "bytes=1093-234546" + }, + "name": "libxcrypt", + "signature": { + "checksum": "sha1-TzqIvPLJxnJXjp9f6Zz6TDjc3vI=", + "range": "bytes=0-696" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libxcrypt-4.4.36-r7.apk", + "version": "4.4.36-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q1u+fF2xg98dT0poYklte5HHMg/Kk=", + "control": { + "checksum": "sha1-u+fF2xg98dT0poYklte5HHMg/Kk=", + "range": "bytes=701-1090" + }, + "data": { + "checksum": "sha256-9e+i7LwSg7oAUU33f0r7A4Mc2suaK95iNU5BKaTSXac=", + "range": "bytes=1091-22179" + }, + "name": "libcrypt1", + "signature": { + "checksum": "sha1-EWBUcJqCJTOYJq1H0/kkO+m6W3E=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libcrypt1-2.39-r7.apk", + "version": "2.39-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q18pQxzg0OXTOCfALb6CR1bxkGqE4=", + "control": { + "checksum": "sha1-8pQxzg0OXTOCfALb6CR1bxkGqE4=", + "range": "bytes=697-1206" + }, + "data": { + "checksum": "sha256-wh9idkYonOQKdIzIFs004NIZW4at4qhO96EqzjR3Ndg=", + "range": "bytes=1207-633845" + }, + "name": "busybox", + "signature": { + "checksum": "sha1-WNnyiq40v7Jhn9yO/7/OQqg+qLo=", + "range": "bytes=0-696" + }, + "url": "https://packages.wolfi.dev/os/x86_64/busybox-1.36.1-r10.apk", + "version": "1.36.1-r10" + }, + { + "architecture": "x86_64", + "checksum": "Q10tocK8jsMh+4qZDmggsMCrDStrY=", + "control": { + "checksum": "sha1-0tocK8jsMh+4qZDmggsMCrDStrY=", + "range": "bytes=706-1112" + }, + "data": { + "checksum": "sha256-VWQYj6T+kz2cFEYgic1vvw5MTcWdiHlJflXF1LcjaUo=", + "range": "bytes=1113-2834738" + }, + "name": "libunistring", + "signature": { + "checksum": "sha1-ZCbGyRE4V3ZkAopIVbsWkHBI+xA=", + "range": "bytes=0-705" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libunistring-1.2-r2.apk", + "version": "1.2-r2" + }, + { + "architecture": "x86_64", + "checksum": "Q1eDLv+3yCKqTHsAZx2HfXxtZb0wA=", + "control": { + "checksum": "sha1-eDLv+3yCKqTHsAZx2HfXxtZb0wA=", + "range": "bytes=704-1122" + }, + "data": { + "checksum": "sha256-ZyU3ZvGcWszw6gOqeBmRP7hSvd/04OCVYK4T33ra8nQ=", + "range": "bytes=1123-389079" + }, + "name": "libidn2", + "signature": { + "checksum": "sha1-KzLzkqsnlKFNJNB9vCxZTTwe3v8=", + "range": "bytes=0-703" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libidn2-2.3.7-r2.apk", + "version": "2.3.7-r2" + }, + { + "architecture": "x86_64", + "checksum": "Q1BRGmQ9LZTeZqbxQ1ls69T6efViw=", + "control": { + "checksum": "sha1-BRGmQ9LZTeZqbxQ1ls69T6efViw=", + "range": "bytes=698-1080" + }, + "data": { + "checksum": "sha256-vtJ4WJ5M+31ZKSBYBedbrl/FrbAw8hGpPRrHC7kasOk=", + "range": "bytes=1081-113895" + }, + "name": "libpsl", + "signature": { + "checksum": "sha1-DuAoiykwhu8vu1qxdEDK5oyAmf8=", + "range": "bytes=0-697" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libpsl-0.21.5-r3.apk", + "version": "0.21.5-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q11DZVpe2UYyDr/dG6BDysZhE3L48=", + "control": { + "checksum": "sha1-1DZVpe2UYyDr/dG6BDysZhE3L48=", + "range": "bytes=707-1049" + }, + "data": { + "checksum": "sha256-aECfsy7giZsLNxZM0HMeF4OS2gu+ycX4DUTJXsiZRQs=", + "range": "bytes=1050-173585" + }, + "name": "libbrotlicommon1", + "signature": { + "checksum": "sha1-7LXD38QwFhklZCEqyW4qIncfZV0=", + "range": "bytes=0-706" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libbrotlicommon1-1.1.0-r3.apk", + "version": "1.1.0-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1Cgr886A8Ryzynz9pxRKsNybVuK0=", + "control": { + "checksum": "sha1-Cgr886A8Ryzynz9pxRKsNybVuK0=", + "range": "bytes=694-1043" + }, + "data": { + "checksum": "sha256-mH+MTbqpLiqgydlgE+tSDEt0Sh1/zqMvAqspV4lr+Ks=", + "range": "bytes=1044-81511" + }, + "name": "libbrotlidec1", + "signature": { + "checksum": "sha1-Yp4Bde9oHQCec8HM2qn6hVKgtko=", + "range": "bytes=0-693" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libbrotlidec1-1.1.0-r3.apk", + "version": "1.1.0-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1pt09x9CUljh5JSXPDhYkq60Zaow=", + "control": { + "checksum": "sha1-pt09x9CUljh5JSXPDhYkq60Zaow=", + "range": "bytes=701-1140" + }, + "data": { + "checksum": "sha256-4XlBbJgUDpdl0hUl3aT2Hh6oe4XsFvXpObtoW6WHomA=", + "range": "bytes=1141-854865" + }, + "name": "libcurl-openssl4", + "signature": { + "checksum": "sha1-w54UwfXCwlH9OS2+9wbDaBYZhEw=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libcurl-openssl4-8.8.0-r0.apk", + "version": "8.8.0-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q19WvKIQk6zRNgQSY+7eCdCyvHuXo=", + "control": { + "checksum": "sha1-9WvKIQk6zRNgQSY+7eCdCyvHuXo=", + "range": "bytes=702-1103" + }, + "data": { + "checksum": "sha256-wXdd4XBkpbkGYrzOTBFB4g7AhU1nkNROqLTsZ6dRcqo=", + "range": "bytes=1104-350301" + }, + "name": "curl", + "signature": { + "checksum": "sha1-nUGVXn+V/TjO6CDCuNwuMwOhHRo=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/curl-8.8.0-r0.apk", + "version": "8.8.0-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1clYRWbDb5ysnWEKiqOQo5PQPHuw=", + "control": { + "checksum": "sha1-clYRWbDb5ysnWEKiqOQo5PQPHuw=", + "range": "bytes=695-1087" + }, + "data": { + "checksum": "sha256-4vCbQ/4Ckn5ChV3LnsNLPEDfbT0RHLZVxedzAZqNA3o=", + "range": "bytes=1088-401764" + }, + "name": "libgomp", + "signature": { + "checksum": "sha1-pPGBt7byb6wIPP8MWkFCKtflzsE=", + "range": "bytes=0-694" + }, + "url": "https://packages.wolfi.dev/os/x86_64/libgomp-13.3.0-r0.apk", + "version": "13.3.0-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1/9ueICeNNFl5V2PdVb5CMQD5xdY=", + "control": { + "checksum": "sha1-/9ueICeNNFl5V2PdVb5CMQD5xdY=", + "range": "bytes=700-1136" + }, + "data": { + "checksum": "sha256-sUFud4B1ij/2uNpP4DyPNeGsowi6EHV9VNbg3HXuqYw=", + "range": "bytes=1137-8472815" + }, + "name": "glibc-iconv", + "signature": { + "checksum": "sha1-wUk7U5fVQXqK1SZaxjFrnaJ2o0k=", + "range": "bytes=0-699" + }, + "url": "https://packages.wolfi.dev/os/x86_64/glibc-iconv-2.39-r7.apk", + "version": "2.39-r7" + }, + { + "architecture": "x86_64", + "checksum": "Q127/3uncINdbAtFvR1Rj/PPfWAOQ=", + "control": { + "checksum": "sha1-27/3uncINdbAtFvR1Rj/PPfWAOQ=", + "range": "bytes=701-1309" + }, + "data": { + "checksum": "sha256-exegcv75ejDtuZrBOkDv9KsY6s1ngvU+XuTSNqPFUz0=", + "range": "bytes=1310-17176274" + }, + "name": "gettext", + "signature": { + "checksum": "sha1-ibnrisnUQXYEPVl+zO+UZsJq2XM=", + "range": "bytes=0-700" + }, + "url": "https://packages.wolfi.dev/os/x86_64/gettext-0.22.5-r0.apk", + "version": "0.22.5-r0" + }, + { + "architecture": "x86_64", + "checksum": "Q1WzXVqTY/neqYGrJ0ddxktnXQnd4=", + "control": { + "checksum": "sha1-WzXVqTY/neqYGrJ0ddxktnXQnd4=", + "range": "bytes=695-1161" + }, + "data": { + "checksum": "sha256-iW6jIVcbBCKo5t4tNP/pMJzTCNrmiXHZe0TEDIU2OTA=", + "range": "bytes=1162-3487050" + }, + "name": "pcre", + "signature": { + "checksum": "sha1-bT3CdFUuwTQ2F/Tixhpqnf1NIrI=", + "range": "bytes=0-694" + }, + "url": "https://packages.wolfi.dev/os/x86_64/pcre-8.45-r3.apk", + "version": "8.45-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1ilPHja4OgIE4Ghwn3Lem3el24a8=", + "control": { + "checksum": "sha1-ilPHja4OgIE4Ghwn3Lem3el24a8=", + "range": "bytes=704-1144" + }, + "data": { + "checksum": "sha256-YE6EOIZDlX8wyqrwatqEuMJ8PqP9oPYzoQXirqTeiQ8=", + "range": "bytes=1145-1341997" + }, + "name": "nginx-mainline", + "signature": { + "checksum": "sha1-1JZ7s+meE/vHPSZDidXATGecqts=", + "range": "bytes=0-703" + }, + "url": "https://packages.wolfi.dev/os/x86_64/nginx-mainline-1.27.0-r5.apk", + "version": "1.27.0-r5" + }, + { + "architecture": "x86_64", + "checksum": "Q16khvLSvBTIM/9zdpX5Xi0z/5fzY=", + "control": { + "checksum": "sha1-6khvLSvBTIM/9zdpX5Xi0z/5fzY=", + "range": "bytes=702-1087" + }, + "data": { + "checksum": "sha256-KTW8s77J6Gl2Buzp3IAJyKbFCW5MUdyV69KfS+Fhcns=", + "range": "bytes=1088-61932" + }, + "name": "nginx-mainline-config", + "signature": { + "checksum": "sha1-8+knAPPIU4yKuPZE0a2oqYp8hnA=", + "range": "bytes=0-701" + }, + "url": "https://packages.wolfi.dev/os/x86_64/nginx-mainline-config-1.27.0-r5.apk", + "version": "1.27.0-r5" + }, + { + "architecture": "x86_64", + "checksum": "Q1mTmy64gKYi4THBqewuvYL2uUyHc=", + "control": { + "checksum": "sha1-mTmy64gKYi4THBqewuvYL2uUyHc=", + "range": "bytes=700-1031" + }, + "data": { + "checksum": "sha256-p0qua2luH33oAgOcLkU9f7k+I8WnykFszVMwNahujDA=", + "range": "bytes=1032-1889523" + }, + "name": "tzdata", + "signature": { + "checksum": "sha1-sBf5jVLVpWj4DiXaAd/mjo0pTfk=", + "range": "bytes=0-699" + }, + "url": "https://packages.wolfi.dev/os/x86_64/tzdata-2024a-r3.apk", + "version": "2024a-r3" + }, + { + "architecture": "x86_64", + "checksum": "Q1opLz4TV9yWwutIxZ2CAQhzWzFrw=", + "control": { + "checksum": "sha1-opLz4TV9yWwutIxZ2CAQhzWzFrw=", + "range": "bytes=707-1109" + }, + "data": { + "checksum": "sha256-Db4mIkfSZnx8qTJH9h9fgrAMbCLfyPlu/KHn7wuf6uQ=", + "range": "bytes=1110-783544" + }, + "name": "wget", + "signature": { + "checksum": "sha1-puv4H1i2MyhmxxEmp0i/tUdF8TI=", + "range": "bytes=0-706" + }, + "url": "https://packages.wolfi.dev/os/x86_64/wget-1.24.5-r3.apk", + "version": "1.24.5-r3" + } + ], + "repositories": [ + { + "architecture": "x86_64", + "name": "packages.wolfi.dev/os/x86_64", + "url": "https://packages.wolfi.dev/os/x86_64/APKINDEX.tar.gz" + }, + { + "architecture": "x86_64", + "name": "@sourcegraph https://packages.sgdev.org/main/x86_64", + "url": "@sourcegraph https://packages.sgdev.org/main/x86_64/APKINDEX.tar.gz" + } + ] + }, + "version": "v1" +} diff --git a/wolfi-images/appliance-frontend.yaml b/wolfi-images/appliance-frontend.yaml new file mode 100755 index 0000000000000..ef00d2c3a0bf7 --- /dev/null +++ b/wolfi-images/appliance-frontend.yaml @@ -0,0 +1,40 @@ +include: ./sourcegraph-template.yaml + +contents: + packages: + - nginx + - gettext + +paths: + - path: /etc/nginx/conf.d + type: directory + uid: 100 + gid: 101 + permissions: 0o755 + - path: /var/lib/nginx/tmp + type: directory + uid: 100 + gid: 101 + permissions: 0o755 + - path: /var/lib/nginx/client_body + type: directory + uid: 100 + gid: 101 + permissions: 0o755 + - path: /run/nginx + type: directory + uid: 100 + gid: 101 + permissions: 0o755 + - path: /var/lib/nginx/logs + type: directory + uid: 100 + gid: 101 + permissions: 0o755 + +annotations: + org.opencontainers.image.url: https://sourcegraph.com/ + org.opencontainers.image.source: https://github.com/sourcegraph/sourcegraph/ + org.opencontainers.image.documentation: https://sourcegraph.com/docs/ + org.opencontainers.image.title: Sourcegraph Appliance Frontend + org.opencontainers.image.description: "all user interface for the Sourcegraph Appliance"