From 5891818ce17f25d5a9b3e56bca560796cf478bf7 Mon Sep 17 00:00:00 2001 From: Tan Nhu Date: Tue, 9 Aug 2022 12:37:37 -0700 Subject: [PATCH] Initial commit --- .gitignore | 9 + .vscode/settings.json | 3 + CHANGELOG.md | 2 + LICENSE.md | 93 + README.md | 111 + Taskfile.yml | 60 + cli/cli.go | 48 + cli/execution/create.go | 82 + cli/execution/delete.go | 40 + cli/execution/execution.go | 17 + cli/execution/find.go | 68 + cli/execution/list.go | 85 + cli/execution/update.go | 87 + cli/login.go | 50 + cli/logout.go | 31 + cli/pipeline/create.go | 77 + cli/pipeline/delete.go | 35 + cli/pipeline/find.go | 63 + cli/pipeline/list.go | 80 + cli/pipeline/pipeline.go | 17 + cli/pipeline/update.go | 82 + cli/register.go | 49 + cli/server/config.go | 38 + cli/server/server.go | 105 + cli/server/system.go | 24 + cli/server/wire.go | 30 + cli/server/wire_gen.go | 34 + cli/swagger.go | 39 + cli/token/token.go | 48 + cli/user/user.go | 63 + cli/users/create.go | 73 + cli/users/delete.go | 35 + cli/users/find.go | 63 + cli/users/list.go | 79 + cli/users/update.go | 107 + cli/users/users.go | 17 + cli/util/util.go | 79 + cli/util/util_test.go | 5 + client/client.go | 336 + client/interface.go | 79 + contrib/README.md | 1 + contrib/kubernetes/spec.yml | 58 + docker/Dockerfile | 12 + docker/Dockerfile.alpine | 12 + go.mod | 51 + go.sum | 632 + internal/api/api.go | 5 + internal/api/handler/account/login.go | 75 + internal/api/handler/account/login_test.go | 23 + internal/api/handler/account/register.go | 103 + internal/api/handler/account/register_test.go | 27 + internal/api/handler/executions/create.go | 96 + .../api/handler/executions/create_test.go | 5 + internal/api/handler/executions/delete.go | 61 + .../api/handler/executions/delete_test.go | 5 + internal/api/handler/executions/find.go | 50 + internal/api/handler/executions/find_test.go | 5 + internal/api/handler/executions/list.go | 55 + internal/api/handler/executions/list_test.go | 5 + internal/api/handler/executions/update.go | 95 + .../api/handler/executions/update_test.go | 5 + internal/api/handler/pipelines/create.go | 80 + internal/api/handler/pipelines/create_test.go | 5 + internal/api/handler/pipelines/delete.go | 46 + internal/api/handler/pipelines/delete_test.go | 5 + internal/api/handler/pipelines/find.go | 51 + internal/api/handler/pipelines/find_test.go | 5 + internal/api/handler/pipelines/list.go | 42 + internal/api/handler/pipelines/list_test.go | 87 + internal/api/handler/pipelines/update.go | 79 + internal/api/handler/pipelines/update_test.go | 5 + internal/api/handler/projects/find.go | 32 + internal/api/handler/projects/find_test.go | 5 + internal/api/handler/projects/list.go | 32 + internal/api/handler/projects/list_test.go | 5 + internal/api/handler/system/health.go | 13 + internal/api/handler/system/health_test.go | 11 + internal/api/handler/system/version.go | 18 + internal/api/handler/system/version_test.go | 11 + internal/api/handler/user/find.go | 34 + internal/api/handler/user/find_test.go | 40 + internal/api/handler/user/token.go | 36 + internal/api/handler/user/token_test.go | 49 + internal/api/handler/user/update.go | 72 + internal/api/handler/user/update_test.go | 196 + internal/api/handler/users/create.go | 80 + internal/api/handler/users/create_test.go | 5 + internal/api/handler/users/delete.go | 45 + internal/api/handler/users/delete_test.go | 5 + internal/api/handler/users/find.go | 35 + internal/api/handler/users/find_test.go | 5 + internal/api/handler/users/list.go | 49 + internal/api/handler/users/list_test.go | 5 + internal/api/handler/users/update.go | 112 + internal/api/handler/users/update_test.go | 5 + internal/api/middleware/access/access.go | 33 + internal/api/middleware/access/access_test.go | 5 + internal/api/middleware/address/address.go | 81 + .../api/middleware/address/address_test.go | 5 + internal/api/middleware/token/token.go | 101 + internal/api/middleware/token/token_test.go | 5 + internal/api/openapi/account.go | 50 + internal/api/openapi/execution.go | 111 + internal/api/openapi/openapi.go | 120 + internal/api/openapi/openapi_test.go | 5 + internal/api/openapi/pipeline.go | 103 + internal/api/openapi/projects.go | 63 + internal/api/openapi/user.go | 56 + internal/api/openapi/users.go | 92 + internal/api/render/errors.go | 33 + internal/api/render/errors_test.go | 14 + internal/api/render/header.go | 68 + internal/api/render/header_test.go | 5 + internal/api/render/platform/render.go | 28 + internal/api/render/render.go | 88 + internal/api/render/render_test.go | 191 + internal/api/render/util.go | 39 + internal/api/render/util_test.go | 28 + internal/api/request/context.go | 34 + internal/api/request/context_test.go | 11 + internal/api/request/util.go | 74 + internal/api/request/util_test.go | 5 + internal/cron/nightly.go | 41 + internal/cron/nightly_test.go | 5 + internal/cron/wire.go | 10 + internal/inernal_test.go | 5 + internal/internal.go | 5 + internal/router/router.go | 204 + internal/router/router_test.go | 28 + internal/router/wire.go | 12 + internal/server/server.go | 124 + internal/server/server_test.go | 5 + internal/server/wire.go | 26 + internal/store/database/execution.go | 145 + internal/store/database/execution_sync.go | 67 + internal/store/database/execution_test.go | 214 + internal/store/database/migrate/migrate.go | 58 + .../0000_create_extension_btree.up.sql | 1 + .../0000_create_extension_citext.up.sql | 1 + .../0000_create_extension_trgm.up.sql | 1 + .../0001_create_table_executions.up.sql | 10 + .../0001_create_table_pipelines.up.sql | 12 + .../postgres/0001_create_table_users.up.sql | 15 + ...02_create_index_executions_pipeline.up.sql | 2 + .../0001_create_table_executions.up.sql | 10 + .../sqlite/0001_create_table_pipelines.up.sql | 12 + .../sqlite/0001_create_table_users.up.sql | 15 + ...02_create_index_executions_pipeline.up.sql | 2 + internal/store/database/mutex/mutex.go | 22 + internal/store/database/pipeline.go | 167 + internal/store/database/pipeline_sync.go | 74 + internal/store/database/pipeline_test.go | 254 + internal/store/database/store.go | 66 + internal/store/database/store_test.go | 63 + .../store/database/testdata/executions.json | 30 + .../store/database/testdata/pipelines.json | 22 + internal/store/database/testdata/users.json | 24 + internal/store/database/user.go | 217 + internal/store/database/user_sync.go | 81 + internal/store/database/user_test.go | 272 + internal/store/database/util.go | 28 + internal/store/database/util_test.go | 76 + internal/store/database/wire.go | 65 + internal/store/memory/config.go | 28 + internal/store/memory/config_test.go | 13 + internal/store/memory/wire.go | 16 + internal/store/store.go | 92 + internal/store/store_test.go | 5 + internal/testing/integration/integration.go | 5 + internal/testing/testing.go | 5 + internal/token/token.go | 46 + internal/token/token_test.go | 107 + main.go | 16 + mocks/mock.go | 9 + mocks/mock_client.go | 317 + mocks/mock_store.go | 425 + node_modules/.yarn-integrity | 15 + scripts/.gitkeep | 0 types/check/execution.go | 48 + types/check/execution_test.go | 5 + types/check/pipeline.go | 48 + types/check/user.go | 25 + types/check/user_test.go | 34 + types/config.go | 67 + types/config_test.go | 5 + types/enum/enum.go | 6 + types/enum/order.go | 42 + types/enum/order_test.go | 34 + types/enum/role.go | 48 + types/enum/role_test.go | 19 + types/enum/user.go | 43 + types/enum/user_test.go | 30 + types/types.go | 170 + types/types_test.go | 5 + version/version.go | 34 + version/version_test.go | 13 + web/.eslintignore | 6 + web/.eslintrc.yml | 126 + web/.gitignore | 5 + web/.prettierrc.yml | 10 + web/.vscode/extensions.json | 3 + web/.vscode/settings.json | 27 + web/README.md | 46 + web/dist.go | 49 + web/jest.config.js | 54 + web/jest.coverage.config.js | 9 + web/package.json | 176 + web/restful-react.config.js | 17 + web/scripts/clean-css-types.js | 39 + .../eslint-rules/duplicate-data-tooltip-id.js | 27 + web/scripts/eslint-rules/jest-no-mock.js | 43 + .../eslint-rules/no-document-body-snapshot.js | 28 + web/scripts/jest/file-mock.js | 1 + web/scripts/jest/gql-loader.js | 10 + web/scripts/jest/setup-file.js | 43 + web/scripts/jest/yaml-transform.js | 9 + web/scripts/lighthouse/lighthouse.js | 214 + web/scripts/setup-github-registry.sh | 21 + web/scripts/strings/generateTypes.cjs | 63 + web/scripts/strings/generateTypesCli.mjs | 5 + web/scripts/swagger-custom-generator.js | 21 + web/scripts/swagger-transform.js | 45 + web/scripts/utils/runPrettier.cjs | 17 + .../webpack/GenerateStringTypesPlugin.js | 18 + web/src/App.scss | 14 + web/src/App.tsx | 72 + web/src/AppContext.tsx | 32 + web/src/AppProps.ts | 78 + web/src/AppUtils.ts | 32 + web/src/RouteDefinitions.ts | 39 + web/src/RouteDestinations.tsx | 75 + web/src/RouteUtils.ts | 59 + web/src/bootstrap.tsx | 10 + .../ContainerSpinner.module.scss | 8 + .../ContainerSpinner.module.scss.d.ts | 6 + .../ContainerSpinner/ContainerSpinner.tsx | 12 + .../EvaluationStatusLabel.module.scss | 42 + .../EvaluationStatusLabel.module.scss.d.ts | 11 + .../EvaluationStatusLabel.tsx | 41 + .../NameIdDescriptionTags.module.scss | 61 + .../NameIdDescriptionTags.module.scss.d.ts | 8 + .../NameIdDescriptionTags.tsx | 223 + .../NameIdDescriptionTagsConstants.ts | 47 + .../OptionsMenuButton/OptionsMenuButton.tsx | 35 + .../components/Settings/Settings.module.scss | 24 + .../Settings/Settings.module.scss.d.ts | 16 + web/src/components/Settings/Settings.tsx | 126 + .../components/SideNav/SideNav.module.scss | 40 + .../SideNav/SideNav.module.scss.d.ts | 16 + web/src/components/SideNav/SideNav.tsx | 38 + web/src/components/Table/Table.module.scss | 11 + .../components/Table/Table.module.scss.d.ts | 14 + web/src/components/Table/Table.tsx | 172 + .../AppErrorBoundary.i18n.json | 8 + .../AppErrorBoundary/AppErrorBoundary.tsx | 59 + web/src/framework/strings/String.tsx | 64 + web/src/framework/strings/StringsContext.tsx | 19 + .../strings/StringsContextProvider.tsx | 43 + .../strings/__tests__/Strings.test.tsx | 163 + web/src/framework/strings/index.ts | 4 + web/src/framework/strings/languageLoader.ts | 17 + web/src/framework/strings/stringTypes.ts | 39 + web/src/global.d.ts | 67 + web/src/hooks/useAPIToken.ts | 17 + web/src/hooks/useLocalStorage.ts | 35 + web/src/hooks/useQueryParams.ts | 24 + web/src/i18n/strings.en.yaml | 34 + web/src/i18n/strings.es.yaml | 0 web/src/index.html | 14 + web/src/index.tsx | 3 + web/src/pages/404/NotFoundPage.tsx | 11 + .../pages/Account/Account.module.scss.d.ts | 15 + web/src/pages/Account/Account.tsx | 153 + web/src/pages/Account/account.module.scss | 17 + web/src/pages/Execution/Settings.tsx | 100 + .../pages/Executions/Executions.module.scss | 30 + .../Executions/Executions.module.scss.d.ts | 16 + web/src/pages/Executions/Executions.tsx | 118 + web/src/pages/Login/Login.tsx | 118 + web/src/pages/Login/login.module.scss | 62 + web/src/pages/Login/login.module.scss.d.ts | 18 + web/src/pages/Pipeline/Settings.tsx | 99 + .../Pipelines/Pipelines.module.scss.d.ts | 16 + web/src/pages/Pipelines/Pipelines.tsx | 112 + web/src/pages/Pipelines/pipelines.module.scss | 30 + .../pages/Register/Register.module.scss.d.ts | 11 + web/src/pages/Register/register.js | 94 + web/src/pages/Register/register.module.scss | 108 + web/src/pages/SignIn/SignIn.tsx | 62 + web/src/services/config.ts | 160 + web/src/services/pm/index.tsx | 718 + web/src/services/pm/overrides.yaml | 2 + web/src/services/pm/swagger.json | 747 ++ web/src/utils/test/testUtils.module.scss | 34 + web/src/utils/test/testUtils.tsx | 167 + web/src/views/TestView/TestView.module.scss | 3 + .../views/TestView/TestView.module.scss.d.ts | 12 + web/src/views/TestView/TestView.tsx | 7 + web/tsconfig-eslint.json | 5 + web/tsconfig.json | 28 + web/webpack.config.js | 269 + web/webpack.devServerProxy.config.js | 16 + web/yarn.lock | 11177 ++++++++++++++++ yarn.lock | 4 + 304 files changed, 28486 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Taskfile.yml create mode 100644 cli/cli.go create mode 100644 cli/execution/create.go create mode 100644 cli/execution/delete.go create mode 100644 cli/execution/execution.go create mode 100644 cli/execution/find.go create mode 100644 cli/execution/list.go create mode 100644 cli/execution/update.go create mode 100644 cli/login.go create mode 100644 cli/logout.go create mode 100644 cli/pipeline/create.go create mode 100644 cli/pipeline/delete.go create mode 100644 cli/pipeline/find.go create mode 100644 cli/pipeline/list.go create mode 100644 cli/pipeline/pipeline.go create mode 100644 cli/pipeline/update.go create mode 100644 cli/register.go create mode 100644 cli/server/config.go create mode 100644 cli/server/server.go create mode 100644 cli/server/system.go create mode 100644 cli/server/wire.go create mode 100644 cli/server/wire_gen.go create mode 100644 cli/swagger.go create mode 100644 cli/token/token.go create mode 100644 cli/user/user.go create mode 100644 cli/users/create.go create mode 100644 cli/users/delete.go create mode 100644 cli/users/find.go create mode 100644 cli/users/list.go create mode 100644 cli/users/update.go create mode 100644 cli/users/users.go create mode 100644 cli/util/util.go create mode 100644 cli/util/util_test.go create mode 100644 client/client.go create mode 100644 client/interface.go create mode 100644 contrib/README.md create mode 100644 contrib/kubernetes/spec.yml create mode 100644 docker/Dockerfile create mode 100644 docker/Dockerfile.alpine create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/api/api.go create mode 100644 internal/api/handler/account/login.go create mode 100644 internal/api/handler/account/login_test.go create mode 100644 internal/api/handler/account/register.go create mode 100644 internal/api/handler/account/register_test.go create mode 100644 internal/api/handler/executions/create.go create mode 100644 internal/api/handler/executions/create_test.go create mode 100644 internal/api/handler/executions/delete.go create mode 100644 internal/api/handler/executions/delete_test.go create mode 100644 internal/api/handler/executions/find.go create mode 100644 internal/api/handler/executions/find_test.go create mode 100644 internal/api/handler/executions/list.go create mode 100644 internal/api/handler/executions/list_test.go create mode 100644 internal/api/handler/executions/update.go create mode 100644 internal/api/handler/executions/update_test.go create mode 100644 internal/api/handler/pipelines/create.go create mode 100644 internal/api/handler/pipelines/create_test.go create mode 100644 internal/api/handler/pipelines/delete.go create mode 100644 internal/api/handler/pipelines/delete_test.go create mode 100644 internal/api/handler/pipelines/find.go create mode 100644 internal/api/handler/pipelines/find_test.go create mode 100644 internal/api/handler/pipelines/list.go create mode 100644 internal/api/handler/pipelines/list_test.go create mode 100644 internal/api/handler/pipelines/update.go create mode 100644 internal/api/handler/pipelines/update_test.go create mode 100644 internal/api/handler/projects/find.go create mode 100644 internal/api/handler/projects/find_test.go create mode 100644 internal/api/handler/projects/list.go create mode 100644 internal/api/handler/projects/list_test.go create mode 100644 internal/api/handler/system/health.go create mode 100644 internal/api/handler/system/health_test.go create mode 100644 internal/api/handler/system/version.go create mode 100644 internal/api/handler/system/version_test.go create mode 100644 internal/api/handler/user/find.go create mode 100644 internal/api/handler/user/find_test.go create mode 100644 internal/api/handler/user/token.go create mode 100644 internal/api/handler/user/token_test.go create mode 100644 internal/api/handler/user/update.go create mode 100644 internal/api/handler/user/update_test.go create mode 100644 internal/api/handler/users/create.go create mode 100644 internal/api/handler/users/create_test.go create mode 100644 internal/api/handler/users/delete.go create mode 100644 internal/api/handler/users/delete_test.go create mode 100644 internal/api/handler/users/find.go create mode 100644 internal/api/handler/users/find_test.go create mode 100644 internal/api/handler/users/list.go create mode 100644 internal/api/handler/users/list_test.go create mode 100644 internal/api/handler/users/update.go create mode 100644 internal/api/handler/users/update_test.go create mode 100644 internal/api/middleware/access/access.go create mode 100644 internal/api/middleware/access/access_test.go create mode 100644 internal/api/middleware/address/address.go create mode 100644 internal/api/middleware/address/address_test.go create mode 100644 internal/api/middleware/token/token.go create mode 100644 internal/api/middleware/token/token_test.go create mode 100644 internal/api/openapi/account.go create mode 100644 internal/api/openapi/execution.go create mode 100644 internal/api/openapi/openapi.go create mode 100644 internal/api/openapi/openapi_test.go create mode 100644 internal/api/openapi/pipeline.go create mode 100644 internal/api/openapi/projects.go create mode 100644 internal/api/openapi/user.go create mode 100644 internal/api/openapi/users.go create mode 100644 internal/api/render/errors.go create mode 100644 internal/api/render/errors_test.go create mode 100644 internal/api/render/header.go create mode 100644 internal/api/render/header_test.go create mode 100644 internal/api/render/platform/render.go create mode 100644 internal/api/render/render.go create mode 100644 internal/api/render/render_test.go create mode 100644 internal/api/render/util.go create mode 100644 internal/api/render/util_test.go create mode 100644 internal/api/request/context.go create mode 100644 internal/api/request/context_test.go create mode 100644 internal/api/request/util.go create mode 100644 internal/api/request/util_test.go create mode 100644 internal/cron/nightly.go create mode 100644 internal/cron/nightly_test.go create mode 100644 internal/cron/wire.go create mode 100644 internal/inernal_test.go create mode 100644 internal/internal.go create mode 100644 internal/router/router.go create mode 100644 internal/router/router_test.go create mode 100644 internal/router/wire.go create mode 100644 internal/server/server.go create mode 100644 internal/server/server_test.go create mode 100644 internal/server/wire.go create mode 100644 internal/store/database/execution.go create mode 100644 internal/store/database/execution_sync.go create mode 100644 internal/store/database/execution_test.go create mode 100644 internal/store/database/migrate/migrate.go create mode 100644 internal/store/database/migrate/postgres/0000_create_extension_btree.up.sql create mode 100644 internal/store/database/migrate/postgres/0000_create_extension_citext.up.sql create mode 100644 internal/store/database/migrate/postgres/0000_create_extension_trgm.up.sql create mode 100644 internal/store/database/migrate/postgres/0001_create_table_executions.up.sql create mode 100644 internal/store/database/migrate/postgres/0001_create_table_pipelines.up.sql create mode 100644 internal/store/database/migrate/postgres/0001_create_table_users.up.sql create mode 100644 internal/store/database/migrate/postgres/0002_create_index_executions_pipeline.up.sql create mode 100644 internal/store/database/migrate/sqlite/0001_create_table_executions.up.sql create mode 100644 internal/store/database/migrate/sqlite/0001_create_table_pipelines.up.sql create mode 100644 internal/store/database/migrate/sqlite/0001_create_table_users.up.sql create mode 100644 internal/store/database/migrate/sqlite/0002_create_index_executions_pipeline.up.sql create mode 100644 internal/store/database/mutex/mutex.go create mode 100644 internal/store/database/pipeline.go create mode 100644 internal/store/database/pipeline_sync.go create mode 100644 internal/store/database/pipeline_test.go create mode 100644 internal/store/database/store.go create mode 100644 internal/store/database/store_test.go create mode 100644 internal/store/database/testdata/executions.json create mode 100644 internal/store/database/testdata/pipelines.json create mode 100644 internal/store/database/testdata/users.json create mode 100644 internal/store/database/user.go create mode 100644 internal/store/database/user_sync.go create mode 100644 internal/store/database/user_test.go create mode 100644 internal/store/database/util.go create mode 100644 internal/store/database/util_test.go create mode 100644 internal/store/database/wire.go create mode 100644 internal/store/memory/config.go create mode 100644 internal/store/memory/config_test.go create mode 100644 internal/store/memory/wire.go create mode 100644 internal/store/store.go create mode 100644 internal/store/store_test.go create mode 100644 internal/testing/integration/integration.go create mode 100644 internal/testing/testing.go create mode 100644 internal/token/token.go create mode 100644 internal/token/token_test.go create mode 100644 main.go create mode 100644 mocks/mock.go create mode 100644 mocks/mock_client.go create mode 100644 mocks/mock_store.go create mode 100644 node_modules/.yarn-integrity create mode 100644 scripts/.gitkeep create mode 100644 types/check/execution.go create mode 100644 types/check/execution_test.go create mode 100644 types/check/pipeline.go create mode 100644 types/check/user.go create mode 100644 types/check/user_test.go create mode 100644 types/config.go create mode 100644 types/config_test.go create mode 100644 types/enum/enum.go create mode 100644 types/enum/order.go create mode 100644 types/enum/order_test.go create mode 100644 types/enum/role.go create mode 100644 types/enum/role_test.go create mode 100644 types/enum/user.go create mode 100644 types/enum/user_test.go create mode 100644 types/types.go create mode 100644 types/types_test.go create mode 100644 version/version.go create mode 100644 version/version_test.go create mode 100644 web/.eslintignore create mode 100644 web/.eslintrc.yml create mode 100644 web/.gitignore create mode 100644 web/.prettierrc.yml create mode 100644 web/.vscode/extensions.json create mode 100644 web/.vscode/settings.json create mode 100644 web/README.md create mode 100644 web/dist.go create mode 100644 web/jest.config.js create mode 100644 web/jest.coverage.config.js create mode 100644 web/package.json create mode 100644 web/restful-react.config.js create mode 100644 web/scripts/clean-css-types.js create mode 100644 web/scripts/eslint-rules/duplicate-data-tooltip-id.js create mode 100644 web/scripts/eslint-rules/jest-no-mock.js create mode 100644 web/scripts/eslint-rules/no-document-body-snapshot.js create mode 100644 web/scripts/jest/file-mock.js create mode 100644 web/scripts/jest/gql-loader.js create mode 100644 web/scripts/jest/setup-file.js create mode 100644 web/scripts/jest/yaml-transform.js create mode 100644 web/scripts/lighthouse/lighthouse.js create mode 100644 web/scripts/setup-github-registry.sh create mode 100644 web/scripts/strings/generateTypes.cjs create mode 100644 web/scripts/strings/generateTypesCli.mjs create mode 100644 web/scripts/swagger-custom-generator.js create mode 100644 web/scripts/swagger-transform.js create mode 100644 web/scripts/utils/runPrettier.cjs create mode 100644 web/scripts/webpack/GenerateStringTypesPlugin.js create mode 100644 web/src/App.scss create mode 100644 web/src/App.tsx create mode 100644 web/src/AppContext.tsx create mode 100644 web/src/AppProps.ts create mode 100644 web/src/AppUtils.ts create mode 100644 web/src/RouteDefinitions.ts create mode 100644 web/src/RouteDestinations.tsx create mode 100644 web/src/RouteUtils.ts create mode 100644 web/src/bootstrap.tsx create mode 100644 web/src/components/ContainerSpinner/ContainerSpinner.module.scss create mode 100644 web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts create mode 100644 web/src/components/ContainerSpinner/ContainerSpinner.tsx create mode 100644 web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss create mode 100644 web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts create mode 100644 web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx create mode 100644 web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss create mode 100644 web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss.d.ts create mode 100644 web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx create mode 100644 web/src/components/NameIdDescriptionTags/NameIdDescriptionTagsConstants.ts create mode 100644 web/src/components/OptionsMenuButton/OptionsMenuButton.tsx create mode 100644 web/src/components/Settings/Settings.module.scss create mode 100644 web/src/components/Settings/Settings.module.scss.d.ts create mode 100644 web/src/components/Settings/Settings.tsx create mode 100644 web/src/components/SideNav/SideNav.module.scss create mode 100644 web/src/components/SideNav/SideNav.module.scss.d.ts create mode 100644 web/src/components/SideNav/SideNav.tsx create mode 100644 web/src/components/Table/Table.module.scss create mode 100644 web/src/components/Table/Table.module.scss.d.ts create mode 100644 web/src/components/Table/Table.tsx create mode 100644 web/src/framework/AppErrorBoundary/AppErrorBoundary.i18n.json create mode 100644 web/src/framework/AppErrorBoundary/AppErrorBoundary.tsx create mode 100644 web/src/framework/strings/String.tsx create mode 100644 web/src/framework/strings/StringsContext.tsx create mode 100644 web/src/framework/strings/StringsContextProvider.tsx create mode 100644 web/src/framework/strings/__tests__/Strings.test.tsx create mode 100644 web/src/framework/strings/index.ts create mode 100644 web/src/framework/strings/languageLoader.ts create mode 100644 web/src/framework/strings/stringTypes.ts create mode 100644 web/src/global.d.ts create mode 100644 web/src/hooks/useAPIToken.ts create mode 100644 web/src/hooks/useLocalStorage.ts create mode 100644 web/src/hooks/useQueryParams.ts create mode 100644 web/src/i18n/strings.en.yaml create mode 100644 web/src/i18n/strings.es.yaml create mode 100644 web/src/index.html create mode 100644 web/src/index.tsx create mode 100644 web/src/pages/404/NotFoundPage.tsx create mode 100644 web/src/pages/Account/Account.module.scss.d.ts create mode 100644 web/src/pages/Account/Account.tsx create mode 100644 web/src/pages/Account/account.module.scss create mode 100644 web/src/pages/Execution/Settings.tsx create mode 100644 web/src/pages/Executions/Executions.module.scss create mode 100644 web/src/pages/Executions/Executions.module.scss.d.ts create mode 100644 web/src/pages/Executions/Executions.tsx create mode 100644 web/src/pages/Login/Login.tsx create mode 100644 web/src/pages/Login/login.module.scss create mode 100644 web/src/pages/Login/login.module.scss.d.ts create mode 100644 web/src/pages/Pipeline/Settings.tsx create mode 100644 web/src/pages/Pipelines/Pipelines.module.scss.d.ts create mode 100644 web/src/pages/Pipelines/Pipelines.tsx create mode 100644 web/src/pages/Pipelines/pipelines.module.scss create mode 100644 web/src/pages/Register/Register.module.scss.d.ts create mode 100644 web/src/pages/Register/register.js create mode 100644 web/src/pages/Register/register.module.scss create mode 100644 web/src/pages/SignIn/SignIn.tsx create mode 100644 web/src/services/config.ts create mode 100644 web/src/services/pm/index.tsx create mode 100644 web/src/services/pm/overrides.yaml create mode 100644 web/src/services/pm/swagger.json create mode 100644 web/src/utils/test/testUtils.module.scss create mode 100644 web/src/utils/test/testUtils.tsx create mode 100644 web/src/views/TestView/TestView.module.scss create mode 100644 web/src/views/TestView/TestView.module.scss.d.ts create mode 100644 web/src/views/TestView/TestView.tsx create mode 100644 web/tsconfig-eslint.json create mode 100644 web/tsconfig.json create mode 100644 web/webpack.config.js create mode 100644 web/webpack.devServerProxy.config.js create mode 100644 web/yarn.lock create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a52445cac --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +NOTES* +_research +.env +*.sqlite +*.sqlite3 +web/node_modules +web/dist/files +release +my-app diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..1252fd6ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.testTags": "sqlite_fts5" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..dfbcd8a29 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this pipeline adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..3015697a5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,93 @@ +# PolyForm Free Trial License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the software +to do everything you might do with the software that would +otherwise infringe the licensor's copyright in it for any +permitted purpose. However, you may only make changes or +new works based on the software according to [Changes and New +Works License](#changes-and-new-works-license), and you may +not distribute copies of the software. + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## Free Trial + +Use to evaluate whether the software suits a particular +application for less than 32 consecutive calendar days, on +behalf of you or your company, is use for a permitted purpose. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +If you violate any of these terms, or do anything with the +software not covered by your licenses, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +organizations that have control over, are under the control of, +or are under common control with that organization. **Control** +means ownership of substantially all the assets of an entity, +or the power to direct its management and policies by vote, +contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..0edd3caa8 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# Pre-Requisites + +Install the latest stable version of Node and Go version 1.17 or higher, and then install the below Go programs. Ensure the GOPATH [bin directory](https://go.dev/doc/gopath_code#GOPATH) is added to your PATH. + +```text +$ go install github.com/golang/mock/mockgen@latest +$ go install github.com/google/wire/cmd/wire@latest +``` + +# Build + +Build the user interface: + +```text +$ pushd web +$ npm install +$ npm run build +$ popd +``` + +Build the server and command line tools: + +```text +$ go generate ./... +$ go build -o release/my-app +``` + +# Test + +Execute the unit tests: + +```text +$ go generate ./... +$ go test -v -cover ./... +``` + +# Run + +This project supports all operating systems and architectures supported by Go. This means you can build and run the system on your machine; docker containers are not required for local development and testing. + +Start the server at `localhost:3000` + +```text +$ release/my-app server +``` + +# User Interface + +This project includes a simple user interface for interacting with the system. When you run the application, you can access the user interface by navigating to `http://localhost:3000` in your browser. + +# Swagger + +This project includes a swagger specification. When you run the application, you can access the swagger specification by navigating to `http://localhost:3000/swagger` in your browser. + +# Command Line + +This project includes simple command line tools for interacting with the system. Please remember that you must start the server before you can execute commands. + +Register a new user: + +```text +$ release/my-app register +``` + +Login to the application: + +```text +$ release/my-app login +``` + +Logout from the application: + +```text +$ release/my-app logout +``` + +View your account details: + +```text +$ release/my-app account +``` + +Generate a peronsal access token: + +```text +$ release/my-app token +``` + +Create a pipeline: + +```text +$ release/my-app pipeline create +``` + +List pipelines: + +```text +$ release/my-app pipeline ls +``` + +Debug and output http responses from the server: + +```text +$ DEBUG=true release/my-app pipeline ls +``` + +View all commands: + +```text +$ release/my-app --help +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 000000000..e0b3b498f --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,60 @@ +version: '3' + +tasks: + test: + cmds: + - go test -v -cover ./... + + # postgres tests are currently failing due to issues + # with the seed.sql file. + test-postgres: + env: + DATABASE_DRIVER: postgres + DATABASE_CONFIG: host=localhost user=postgres password=postgres dbname=postgres sslmode=disable + GO111MODULE: 'on' + cmds: + - cmd: docker kill postgres + ignore_error: true + silent: false + - silent: false + cmd: > + docker run + -p 5432:5432 + --env POSTGRES_PASSWORD=postgres + --env POSTGRES_USER=postgres + --name postgres + --detach + --rm + postgres:9-alpine + - cmd: go test -v -cover github.com/bradrydzewski/my-app/internal/store/database + - cmd: docker kill postgres + silent: true + + setup: + cmds: + - cd web; npm install + - cd web; npm run build + - go generate ./... + - go build + + teardown: + cmds: + - rm -rf release + - rm -rf web/.cache + - rm -rf web/dist/files + - rm -rf web/node_modules + - rm -rf web/.env.development.local + - rm -rf web/swagger.yaml + - rm -rf my-app + - rm -rf database.sqlite3 + - rm -rf .env + + docker-build: + cmds: + - docker build -t bradrydzewski/my-app:linux-amd64 -f docker/Dockerfile . + + docker-build-all: + cmds: + - docker build -t bradrydzewski/my-app:linux-amd64 -f docker/Dockerfile . + - docker build -t bradrydzewski/my-app:linux-arm64 -f docker/Dockerfile.linux.arm64 . + - docker build -t bradrydzewski/my-app:linux-arm -f docker/Dockerfile.linux.arm . diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 000000000..0f299868f --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,48 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cli + +import ( + "context" + "os" + + "github.com/bradrydzewski/my-app/cli/execution" + "github.com/bradrydzewski/my-app/cli/pipeline" + "github.com/bradrydzewski/my-app/cli/server" + "github.com/bradrydzewski/my-app/cli/token" + "github.com/bradrydzewski/my-app/cli/user" + "github.com/bradrydzewski/my-app/cli/users" + "github.com/bradrydzewski/my-app/version" + + "gopkg.in/alecthomas/kingpin.v2" +) + +// empty context +var nocontext = context.Background() + +// application name +var application = "my-app" + +// application description +var description = "description goes here" // TODO edit this application description + +// Command parses the command line arguments and then executes a +// subcommand program. +func Command() { + app := kingpin.New(application, description) + server.Register(app) + user.Register(app) + pipeline.Register(app) + execution.Register(app) + users.Register(app) + token.Register(app) + registerLogin(app) + registerLogout(app) + registerRegister(app) + registerSwagger(app) + + kingpin.Version(version.Version.String()) + kingpin.MustParse(app.Parse(os.Args[1:])) +} diff --git a/cli/execution/create.go b/cli/execution/create.go new file mode 100644 index 000000000..0c25b66de --- /dev/null +++ b/cli/execution/create.go @@ -0,0 +1,82 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type createCommand struct { + pipeline string + slug string + name string + desc string + tmpl string + json bool +} + +func (c *createCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + in := &types.Execution{ + Slug: c.slug, + Name: c.name, + Desc: c.desc, + } + item, err := client.ExecutionCreate(c.pipeline, in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the user create command +func registerCreate(app *kingpin.CmdClause) { + c := new(createCommand) + + cmd := app.Command("create", "create a execution"). + Action(c.run) + + cmd.Arg("pipeline ", "pipeline slug"). + Required(). + StringVar(&c.pipeline) + + cmd.Arg("slug ", "execution slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("name", "execution name"). + StringVar(&c.name) + + cmd.Flag("desc", "execution description"). + StringVar(&c.desc) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(executionTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/execution/delete.go b/cli/execution/delete.go new file mode 100644 index 000000000..b01ac943c --- /dev/null +++ b/cli/execution/delete.go @@ -0,0 +1,40 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "github.com/bradrydzewski/my-app/cli/util" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type deleteCommand struct { + pipeline string + slug string +} + +func (c *deleteCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + return client.ExecutionDelete(c.pipeline, c.slug) +} + +// helper function registers the user delete command +func registerDelete(app *kingpin.CmdClause) { + c := new(deleteCommand) + + cmd := app.Command("delete", "delete a execution"). + Action(c.run) + + cmd.Arg("pipeline ", "pipeline slug"). + Required(). + StringVar(&c.pipeline) + + cmd.Arg("slug ", "execution slug"). + Required(). + StringVar(&c.slug) +} diff --git a/cli/execution/execution.go b/cli/execution/execution.go new file mode 100644 index 000000000..826c76a98 --- /dev/null +++ b/cli/execution/execution.go @@ -0,0 +1,17 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import "gopkg.in/alecthomas/kingpin.v2" + +// Register the command. +func Register(app *kingpin.Application) { + cmd := app.Command("execution", "manage executions") + registerFind(cmd) + registerList(cmd) + registerCreate(cmd) + registerUpdate(cmd) + registerDelete(cmd) +} diff --git a/cli/execution/find.go b/cli/execution/find.go new file mode 100644 index 000000000..4ac73a911 --- /dev/null +++ b/cli/execution/find.go @@ -0,0 +1,68 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type findCommand struct { + pipeline string + slug string + tmpl string + json bool +} + +func (c *findCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + item, err := client.Execution(c.pipeline, c.slug) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the user find command +func registerFind(app *kingpin.CmdClause) { + c := new(findCommand) + + cmd := app.Command("find", "display pipeline details"). + Action(c.run) + + cmd.Arg("pipeline ", "pipeline slug"). + Required(). + StringVar(&c.pipeline) + + cmd.Arg("slug ", "execution slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(executionTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/execution/list.go b/cli/execution/list.go new file mode 100644 index 000000000..bc1821603 --- /dev/null +++ b/cli/execution/list.go @@ -0,0 +1,85 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + "github.com/drone/funcmap" + + "gopkg.in/alecthomas/kingpin.v2" +) + +const executionTmpl = ` +id: {{ .ID }} +slug: {{ .Slug }} +name: {{ .Name }} +desc: {{ .Desc }} +` + +type listCommand struct { + slug string + tmpl string + json bool + page int + size int +} + +func (c *listCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + list, err := client.ExecutionList(c.slug, types.Params{ + Size: c.size, + Page: c.page, + }) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(list) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + for _, item := range list { + tmpl.Execute(os.Stdout, item) + } + return nil +} + +// helper function registers the list command +func registerList(app *kingpin.CmdClause) { + c := new(listCommand) + + cmd := app.Command("ls", "display a list of executions"). + Action(c.run) + + cmd.Arg("pipeline ", "pipeline slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("page", "page number"). + IntVar(&c.page) + + cmd.Flag("per-page", "page size"). + IntVar(&c.size) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(executionTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/execution/update.go b/cli/execution/update.go new file mode 100644 index 000000000..654098134 --- /dev/null +++ b/cli/execution/update.go @@ -0,0 +1,87 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + "github.com/gotidy/ptr" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type updateCommand struct { + pipeline string + slug string + name string + desc string + tmpl string + json bool +} + +func (c *updateCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + + in := new(types.ExecutionInput) + if v := c.name; v != "" { + in.Name = ptr.String(v) + } + if v := c.desc; v != "" { + in.Desc = ptr.String(v) + } + + item, err := client.ExecutionUpdate(c.pipeline, c.slug, in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the update command +func registerUpdate(app *kingpin.CmdClause) { + c := new(updateCommand) + + cmd := app.Command("update", "update a execution"). + Action(c.run) + + cmd.Arg("pipeline ", "pipeline slug"). + Required(). + StringVar(&c.pipeline) + + cmd.Arg("slug ", "execution slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("name", "update pipeline name"). + StringVar(&c.name) + + cmd.Flag("desc", "update pipeline description"). + StringVar(&c.desc) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(executionTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/login.go b/cli/login.go new file mode 100644 index 000000000..da2eabe41 --- /dev/null +++ b/cli/login.go @@ -0,0 +1,50 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cli + +import ( + "encoding/json" + "io/ioutil" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/client" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type loginCommand struct { + server string +} + +func (c *loginCommand) run(*kingpin.ParseContext) error { + username, password := util.Credentials() + client := client.New(c.server) + token, err := client.Login(username, password) + if err != nil { + return err + } + path, err := util.Config() + if err != nil { + return err + } + token.Address = c.server + data, err := json.Marshal(token) + if err != nil { + return err + } + return ioutil.WriteFile(path, data, 0600) +} + +// helper function to register the logout command. +func registerLogin(app *kingpin.Application) { + c := new(loginCommand) + + cmd := app.Command("login", "login to the remote server"). + Action(c.run) + + cmd.Arg("server", "server address"). + Default("http://localhost:3000"). + StringVar(&c.server) +} diff --git a/cli/logout.go b/cli/logout.go new file mode 100644 index 000000000..6eb9ecfcd --- /dev/null +++ b/cli/logout.go @@ -0,0 +1,31 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cli + +import ( + "os" + + "github.com/bradrydzewski/my-app/cli/util" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type logoutCommand struct{} + +func (c *logoutCommand) run(*kingpin.ParseContext) error { + path, err := util.Config() + if err != nil { + return err + } + return os.Remove(path) +} + +// helper function to register the logout command. +func registerLogout(app *kingpin.Application) { + c := new(logoutCommand) + + app.Command("logout", "logout from the remote server"). + Action(c.run) +} diff --git a/cli/pipeline/create.go b/cli/pipeline/create.go new file mode 100644 index 000000000..3548973f4 --- /dev/null +++ b/cli/pipeline/create.go @@ -0,0 +1,77 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type createCommand struct { + slug string + name string + desc string + tmpl string + json bool +} + +func (c *createCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + in := &types.Pipeline{ + Slug: c.slug, + Name: c.name, + Desc: c.desc, + } + item, err := client.PipelineCreate(in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the user create command +func registerCreate(app *kingpin.CmdClause) { + c := new(createCommand) + + cmd := app.Command("create", "create a pipeline"). + Action(c.run) + + cmd.Arg("slug", "pipeline slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("name", "pipeline name"). + StringVar(&c.name) + + cmd.Flag("desc", "pipeline description"). + StringVar(&c.desc) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(pipelineTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/pipeline/delete.go b/cli/pipeline/delete.go new file mode 100644 index 000000000..5e3f0e14b --- /dev/null +++ b/cli/pipeline/delete.go @@ -0,0 +1,35 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "github.com/bradrydzewski/my-app/cli/util" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type deleteCommand struct { + slug string +} + +func (c *deleteCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + return client.PipelineDelete(c.slug) +} + +// helper function registers the user delete command +func registerDelete(app *kingpin.CmdClause) { + c := new(deleteCommand) + + cmd := app.Command("delete", "delete a pipeline"). + Action(c.run) + + cmd.Arg("slug ", "pipeline slug"). + Required(). + StringVar(&c.slug) +} diff --git a/cli/pipeline/find.go b/cli/pipeline/find.go new file mode 100644 index 000000000..c2bd52b2a --- /dev/null +++ b/cli/pipeline/find.go @@ -0,0 +1,63 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type findCommand struct { + slug string + tmpl string + json bool +} + +func (c *findCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + item, err := client.Pipeline(c.slug) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the user find command +func registerFind(app *kingpin.CmdClause) { + c := new(findCommand) + + cmd := app.Command("find", "display pipeline details"). + Action(c.run) + + cmd.Arg("slug", "pipeline slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(pipelineTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/pipeline/list.go b/cli/pipeline/list.go new file mode 100644 index 000000000..9f56c1f8d --- /dev/null +++ b/cli/pipeline/list.go @@ -0,0 +1,80 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + "github.com/drone/funcmap" + + "gopkg.in/alecthomas/kingpin.v2" +) + +const pipelineTmpl = ` +id: {{ .ID }} +slug: {{ .Slug }} +name: {{ .Name }} +desc: {{ .Desc }} +` + +type listCommand struct { + page int + size int + json bool + tmpl string +} + +func (c *listCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + list, err := client.PipelineList(types.Params{ + Size: c.size, + Page: c.page, + }) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(list) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + for _, item := range list { + tmpl.Execute(os.Stdout, item) + } + return nil +} + +// helper function registers the user list command +func registerList(app *kingpin.CmdClause) { + c := new(listCommand) + + cmd := app.Command("ls", "display a list of pipelines"). + Action(c.run) + + cmd.Flag("page", "page number"). + IntVar(&c.page) + + cmd.Flag("per-page", "page size"). + IntVar(&c.size) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(pipelineTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/pipeline/pipeline.go b/cli/pipeline/pipeline.go new file mode 100644 index 000000000..810b1b979 --- /dev/null +++ b/cli/pipeline/pipeline.go @@ -0,0 +1,17 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import "gopkg.in/alecthomas/kingpin.v2" + +// Register the command. +func Register(app *kingpin.Application) { + cmd := app.Command("pipeline", "manage pipelines") + registerFind(cmd) + registerList(cmd) + registerCreate(cmd) + registerUpdate(cmd) + registerDelete(cmd) +} diff --git a/cli/pipeline/update.go b/cli/pipeline/update.go new file mode 100644 index 000000000..4af9ad120 --- /dev/null +++ b/cli/pipeline/update.go @@ -0,0 +1,82 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + "github.com/gotidy/ptr" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type updateCommand struct { + slug string + name string + desc string + tmpl string + json bool +} + +func (c *updateCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + + in := new(types.PipelineInput) + if v := c.name; v != "" { + in.Name = ptr.String(v) + } + if v := c.desc; v != "" { + in.Desc = ptr.String(v) + } + + item, err := client.PipelineUpdate(c.slug, in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(item) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, item) +} + +// helper function registers the user update command +func registerUpdate(app *kingpin.CmdClause) { + c := new(updateCommand) + + cmd := app.Command("update", "update a pipeline"). + Action(c.run) + + cmd.Arg("slug", "pipeline slug"). + Required(). + StringVar(&c.slug) + + cmd.Flag("name", "update pipeline name"). + StringVar(&c.name) + + cmd.Flag("desc", "update pipeline description"). + StringVar(&c.desc) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(pipelineTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/register.go b/cli/register.go new file mode 100644 index 000000000..e48eca178 --- /dev/null +++ b/cli/register.go @@ -0,0 +1,49 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cli + +import ( + "encoding/json" + "io/ioutil" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/client" + "gopkg.in/alecthomas/kingpin.v2" +) + +type registerCommand struct { + server string +} + +func (c *registerCommand) run(*kingpin.ParseContext) error { + username, password := util.Credentials() + client := client.New(c.server) + token, err := client.Register(username, password) + if err != nil { + return err + } + path, err := util.Config() + if err != nil { + return err + } + token.Address = c.server + data, err := json.Marshal(token) + if err != nil { + return err + } + return ioutil.WriteFile(path, data, 0600) +} + +// helper function to register the register command. +func registerRegister(app *kingpin.Application) { + c := new(registerCommand) + + cmd := app.Command("register", "register a user"). + Action(c.run) + + cmd.Arg("server", "server address"). + Default("http://localhost:3000"). + StringVar(&c.server) +} diff --git a/cli/server/config.go b/cli/server/config.go new file mode 100644 index 000000000..e7541d9c5 --- /dev/null +++ b/cli/server/config.go @@ -0,0 +1,38 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package server + +import ( + "os" + + "github.com/bradrydzewski/my-app/types" + + "github.com/kelseyhightower/envconfig" +) + +// legacy environment variables. the key is the legacy +// variable name, and the value is the new variable name. +var legacy = map[string]string{ + // none defined +} + +// load returns the system configuration from the +// host environment. +func load() (*types.Config, error) { + + // loop through legacy environment variable and, if set + // rewrite to the new variable name. + for k, v := range legacy { + if s, ok := os.LookupEnv(k); ok { + os.Setenv(v, s) + } + } + + config := new(types.Config) + // read the configuration from the environment and + // populate the configuration structure. + err := envconfig.Process("", config) + return config, err +} diff --git a/cli/server/server.go b/cli/server/server.go new file mode 100644 index 000000000..b9470e596 --- /dev/null +++ b/cli/server/server.go @@ -0,0 +1,105 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package server + +import ( + "context" + "os" + + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/version" + + "github.com/joho/godotenv" + "github.com/mattn/go-isatty" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "golang.org/x/sync/errgroup" + "gopkg.in/alecthomas/kingpin.v2" +) + +type command struct { + envfile string +} + +func (c *command) run(*kingpin.ParseContext) error { + // load environment variables from file. + godotenv.Load(c.envfile) + + // create the system configuration store by loading + // data from the environment. + config, err := load() + if err != nil { + log.Fatal().Err(err). + Msg("cannot load configuration") + } + + // configure the log level + setupLogger(config) + + system, err := initSystem(config) + if err != nil { + log.Fatal().Err(err). + Msg("cannot boot server") + } + + var g errgroup.Group + + // starts the http server. + g.Go(func() error { + log.Info(). + Str("port", config.Server.Bind). + Str("revision", version.GitCommit). + Str("repository", version.GitRepository). + Stringer("version", version.Version). + Msg("server started") + return system.server.ListenAndServe(context.Background()) + }) + + // start the purge routine. + g.Go(func() error { + log.Debug().Msg("starting the nightly subroutine") + system.nightly.Run(context.Background()) + return nil + }) + + return g.Wait() +} + +// helper function configures the global logger from +// the loaded configuration. +func setupLogger(config *types.Config) { + // configure the log level + switch { + case config.Trace: + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case config.Debug: + zerolog.SetGlobalLevel(zerolog.DebugLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + + // if the terminal is a tty we should output the + // logs in pretty format + if isatty.IsTerminal(os.Stdout.Fd()) { + log.Logger = log.Output( + zerolog.ConsoleWriter{ + Out: os.Stderr, + NoColor: false, + }, + ) + } +} + +// Register the server command. +func Register(app *kingpin.Application) { + c := new(command) + + cmd := app.Command("server", "starts the server"). + Action(c.run) + + cmd.Arg("envfile", "load the environment variable file"). + Default(""). + StringVar(&c.envfile) +} diff --git a/cli/server/system.go b/cli/server/system.go new file mode 100644 index 000000000..07de47506 --- /dev/null +++ b/cli/server/system.go @@ -0,0 +1,24 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package server + +import ( + "github.com/bradrydzewski/my-app/internal/cron" + "github.com/bradrydzewski/my-app/internal/server" +) + +// system stores high level system sub-routines. +type system struct { + server *server.Server + nightly *cron.Nightly +} + +// newSystem returns a new system structure. +func newSystem(server *server.Server, nightly *cron.Nightly) *system { + return &system{ + server: server, + nightly: nightly, + } +} diff --git a/cli/server/wire.go b/cli/server/wire.go new file mode 100644 index 000000000..9498362e1 --- /dev/null +++ b/cli/server/wire.go @@ -0,0 +1,30 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +//+build wireinject + +package server + +import ( + "github.com/bradrydzewski/my-app/internal/cron" + "github.com/bradrydzewski/my-app/internal/router" + "github.com/bradrydzewski/my-app/internal/server" + "github.com/bradrydzewski/my-app/internal/store/database" + "github.com/bradrydzewski/my-app/internal/store/memory" + "github.com/bradrydzewski/my-app/types" + + "github.com/google/wire" +) + +func initSystem(config *types.Config) (*system, error) { + wire.Build( + database.WireSet, + memory.WireSet, + router.WireSet, + server.WireSet, + cron.WireSet, + newSystem, + ) + return &system{}, nil +} diff --git a/cli/server/wire_gen.go b/cli/server/wire_gen.go new file mode 100644 index 000000000..6ff70ef9f --- /dev/null +++ b/cli/server/wire_gen.go @@ -0,0 +1,34 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package server + +import ( + "github.com/bradrydzewski/my-app/internal/cron" + "github.com/bradrydzewski/my-app/internal/router" + "github.com/bradrydzewski/my-app/internal/server" + "github.com/bradrydzewski/my-app/internal/store/database" + "github.com/bradrydzewski/my-app/internal/store/memory" + "github.com/bradrydzewski/my-app/types" +) + +// Injectors from wire.go: + +func initSystem(config *types.Config) (*system, error) { + db, err := database.ProvideDatabase(config) + if err != nil { + return nil, err + } + executionStore := database.ProvideExecutionStore(db) + pipelineStore := database.ProvidePipelineStore(db) + userStore := database.ProvideUserStore(db) + systemStore := memory.New(config) + handler := router.New(executionStore, pipelineStore, userStore, systemStore) + serverServer := server.ProvideServer(config, handler) + nightly := cron.NewNightly() + serverSystem := newSystem(serverServer, nightly) + return serverSystem, nil +} diff --git a/cli/swagger.go b/cli/swagger.go new file mode 100644 index 000000000..f1f2d4d7d --- /dev/null +++ b/cli/swagger.go @@ -0,0 +1,39 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cli + +import ( + "io/ioutil" + "os" + + "github.com/bradrydzewski/my-app/internal/api/openapi" + "gopkg.in/alecthomas/kingpin.v2" +) + +type swaggerCommand struct { + path string +} + +func (c *swaggerCommand) run(*kingpin.ParseContext) error { + spec := openapi.Generate() + data, _ := spec.MarshalYAML() + if c.path == "" { + os.Stdout.Write(data) + return nil + } + return ioutil.WriteFile(c.path, data, 0600) +} + +// helper function to register the swagger command. +func registerSwagger(app *kingpin.Application) { + c := new(swaggerCommand) + + cmd := app.Command("swagger", "generate swagger file"). + Hidden(). + Action(c.run) + + cmd.Arg("path", "path to save swagger file"). + StringVar(&c.path) +} diff --git a/cli/token/token.go b/cli/token/token.go new file mode 100644 index 000000000..0cc721760 --- /dev/null +++ b/cli/token/token.go @@ -0,0 +1,48 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package token + +import ( + "encoding/json" + "os" + + "github.com/bradrydzewski/my-app/cli/util" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type command struct { + json bool +} + +func (c *command) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + token, err := client.Token() + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(token) + } + println(token.Value) + return nil +} + +// Register the command. +func Register(app *kingpin.Application) { + c := new(command) + + cmd := app.Command("token", "generate a personal token"). + Action(c.run) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + +} diff --git a/cli/user/user.go b/cli/user/user.go new file mode 100644 index 000000000..cee7b323d --- /dev/null +++ b/cli/user/user.go @@ -0,0 +1,63 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +const userTmpl = ` +email: {{ .Email }} +admin: {{ .Admin }} +` + +type command struct { + tmpl string + json bool +} + +func (c *command) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + user, err := client.Self() + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(user) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, user) +} + +// Register the command. +func Register(app *kingpin.Application) { + c := new(command) + + cmd := app.Command("account", "display authenticated user"). + Action(c.run) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(userTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/users/create.go b/cli/users/create.go new file mode 100644 index 000000000..f30f3a3bb --- /dev/null +++ b/cli/users/create.go @@ -0,0 +1,73 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type createCommand struct { + email string + admin bool + tmpl string + json bool +} + +func (c *createCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + in := &types.User{ + Admin: c.admin, + Email: c.email, + Password: util.Password(), + } + user, err := client.UserCreate(in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(user) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, user) +} + +// helper function registers the user create command +func registerCreate(app *kingpin.CmdClause) { + c := new(createCommand) + + cmd := app.Command("create", "create a user"). + Action(c.run) + + cmd.Arg("email", "user email"). + Required(). + StringVar(&c.email) + + cmd.Arg("admin", "user is admin"). + BoolVar(&c.admin) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(userTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/users/delete.go b/cli/users/delete.go new file mode 100644 index 000000000..2b27e1c2c --- /dev/null +++ b/cli/users/delete.go @@ -0,0 +1,35 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "github.com/bradrydzewski/my-app/cli/util" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type deleteCommand struct { + email string +} + +func (c *deleteCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + return client.UserDelete(c.email) +} + +// helper function registers the user delete command +func registerDelete(app *kingpin.CmdClause) { + c := new(deleteCommand) + + cmd := app.Command("delete", "delete a user"). + Action(c.run) + + cmd.Arg("id or email", "user id or email"). + Required(). + StringVar(&c.email) +} diff --git a/cli/users/find.go b/cli/users/find.go new file mode 100644 index 000000000..adb0f672d --- /dev/null +++ b/cli/users/find.go @@ -0,0 +1,63 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + + "github.com/drone/funcmap" + "gopkg.in/alecthomas/kingpin.v2" +) + +type findCommand struct { + email string + tmpl string + json bool +} + +func (c *findCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + user, err := client.User(c.email) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(user) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, user) +} + +// helper function registers the user find command +func registerFind(app *kingpin.CmdClause) { + c := new(findCommand) + + cmd := app.Command("find", "display user details"). + Action(c.run) + + cmd.Arg("id or email", "user id or email"). + Required(). + StringVar(&c.email) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(userTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/users/list.go b/cli/users/list.go new file mode 100644 index 000000000..1c1f2d15d --- /dev/null +++ b/cli/users/list.go @@ -0,0 +1,79 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + "github.com/drone/funcmap" + + "gopkg.in/alecthomas/kingpin.v2" +) + +const userTmpl = ` +id: {{ .ID }} +email: {{ .Email }} +admin: {{ .Admin }} +` + +type listCommand struct { + tmpl string + page int + size int + json bool +} + +func (c *listCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + list, err := client.UserList(types.Params{ + Size: c.size, + Page: c.page, + }) + if err != nil { + return err + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl + "\n") + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(list) + } + for _, item := range list { + tmpl.Execute(os.Stdout, item) + } + return nil +} + +// helper function registers the user list command +func registerList(app *kingpin.CmdClause) { + c := new(listCommand) + + cmd := app.Command("ls", "display a list of users"). + Action(c.run) + + cmd.Flag("page", "page number"). + IntVar(&c.page) + + cmd.Flag("per-page", "page size"). + IntVar(&c.size) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(userTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/users/update.go b/cli/users/update.go new file mode 100644 index 000000000..dbde3ba56 --- /dev/null +++ b/cli/users/update.go @@ -0,0 +1,107 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "fmt" + "os" + "text/template" + + "github.com/bradrydzewski/my-app/cli/util" + "github.com/bradrydzewski/my-app/types" + + "github.com/dchest/uniuri" + "github.com/drone/funcmap" + "github.com/gotidy/ptr" + "gopkg.in/alecthomas/kingpin.v2" +) + +type updateCommand struct { + id string + email string + admin bool + demote bool + passgen bool + pass string + tmpl string + json bool +} + +func (c *updateCommand) run(*kingpin.ParseContext) error { + client, err := util.Client() + if err != nil { + return err + } + + in := new(types.UserInput) + if v := c.email; v != "" { + in.Username = ptr.String(v) + } + if v := c.pass; v != "" { + in.Password = ptr.String(v) + } + if v := c.admin; v { + in.Admin = ptr.Bool(v) + } + if v := c.demote; v { + in.Admin = ptr.Bool(false) + } + if c.passgen { + v := uniuri.NewLen(8) + in.Password = ptr.String(v) + fmt.Printf("generated temporary password: %s\n", v) + } + + user, err := client.UserUpdate(c.id, in) + if err != nil { + return err + } + if c.json { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(user) + } + tmpl, err := template.New("_").Funcs(funcmap.Funcs).Parse(c.tmpl) + if err != nil { + return err + } + return tmpl.Execute(os.Stdout, user) +} + +// helper function registers the user update command +func registerUpdate(app *kingpin.CmdClause) { + c := new(updateCommand) + + cmd := app.Command("update", "update a user"). + Action(c.run) + + cmd.Arg("id or email", "user id or email"). + Required(). + StringVar(&c.id) + + cmd.Flag("email", "update user email"). + StringVar(&c.email) + + cmd.Flag("password", "update user password"). + StringVar(&c.pass) + + cmd.Flag("password-gen", "generate and update user password"). + BoolVar(&c.passgen) + + cmd.Flag("promote", "promote user to admin"). + BoolVar(&c.admin) + + cmd.Flag("demote", "demote user from admin"). + BoolVar(&c.demote) + + cmd.Flag("json", "json encode the output"). + BoolVar(&c.json) + + cmd.Flag("format", "format the output using a Go template"). + Default(userTmpl). + Hidden(). + StringVar(&c.tmpl) +} diff --git a/cli/users/users.go b/cli/users/users.go new file mode 100644 index 000000000..3efa63498 --- /dev/null +++ b/cli/users/users.go @@ -0,0 +1,17 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import "gopkg.in/alecthomas/kingpin.v2" + +// Register the command. +func Register(app *kingpin.Application) { + cmd := app.Command("user", "manage users") + registerFind(cmd) + registerList(cmd) + registerCreate(cmd) + registerUpdate(cmd) + registerDelete(cmd) +} diff --git a/cli/util/util.go b/cli/util/util.go new file mode 100644 index 000000000..ea1ec2776 --- /dev/null +++ b/cli/util/util.go @@ -0,0 +1,79 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package util + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/bradrydzewski/my-app/client" + "github.com/bradrydzewski/my-app/types" + + "github.com/adrg/xdg" + "golang.org/x/crypto/ssh/terminal" +) + +// Client returns a client that is configured from file. +func Client() (*client.HTTPClient, error) { + path, err := Config() + if err != nil { + return nil, err + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + token := new(types.Token) + if err := json.Unmarshal(data, token); err != nil { + return nil, err + } + if time.Now().Unix() > token.Expires.Unix() { + return nil, errors.New("token is expired, please login") + } + client := client.NewToken(token.Address, token.Value) + if os.Getenv("DEBUG") == "true" { + client.SetDebug(true) + } + return client, nil +} + +// Config returns the configuration file path. +func Config() (string, error) { + return xdg.ConfigFile( + filepath.Join("app", "config.json"), + ) +} + +// Credentials returns the username and password from stdin. +func Credentials() (string, string) { + return Username(), Password() +} + +// Username returns the username from stdin. +func Username() string { + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter Username: ") + username, _ := reader.ReadString('\n') + + return strings.TrimSpace(username) +} + +// Password returns the password from stdin. +func Password() string { + fmt.Print("Enter Password: ") + passwordb, _ := terminal.ReadPassword(int(syscall.Stdin)) + password := string(passwordb) + + return strings.TrimSpace(password) +} diff --git a/cli/util/util_test.go b/cli/util/util_test.go new file mode 100644 index 000000000..18d0551cf --- /dev/null +++ b/cli/util/util_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package util diff --git a/client/client.go b/client/client.go new file mode 100644 index 000000000..37d67bc12 --- /dev/null +++ b/client/client.go @@ -0,0 +1,336 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/version" +) + +// ensure HTTPClient implements Client interface. +var _ Client = (*HTTPClient)(nil) + +// HTTPClient provides an HTTP client for interacting +// with the remote API. +type HTTPClient struct { + client *http.Client + base string + token string + debug bool +} + +// New returns a client at the specified url. +func New(uri string) *HTTPClient { + return NewToken(uri, "") +} + +// NewToken returns a client at the specified url that +// authenticates all outbound requests with the given token. +func NewToken(uri, token string) *HTTPClient { + return &HTTPClient{http.DefaultClient, uri, token, false} +} + +// SetClient sets the default http client. This can be +// used in conjunction with golang.org/x/oauth2 to +// authenticate requests to the server. +func (c *HTTPClient) SetClient(client *http.Client) { + c.client = client +} + +// SetDebug sets the debug flag. When the debug flag is +// true, the http.Resposne body to stdout which can be +// helpful when debugging. +func (c *HTTPClient) SetDebug(debug bool) { + c.debug = debug +} + +// Login authenticates the user and returns a JWT token. +func (c *HTTPClient) Login(username, password string) (*types.Token, error) { + form := &url.Values{} + form.Add("username", username) + form.Add("password", password) + out := new(types.UserToken) + uri := fmt.Sprintf("%s/api/v1/login?return_user=true", c.base) + err := c.post(uri, form, out) + return out.Token, err +} + +// Register registers a new user and returns a JWT token. +func (c *HTTPClient) Register(username, password string) (*types.Token, error) { + form := &url.Values{} + form.Add("username", username) + form.Add("password", password) + out := new(types.UserToken) + uri := fmt.Sprintf("%s/api/v1/register?return_user=true", c.base) + err := c.post(uri, form, out) + return out.Token, err +} + +// +// User Endpoints +// + +// Self returns the currently authenticated user. +func (c *HTTPClient) Self() (*types.User, error) { + out := new(types.User) + uri := fmt.Sprintf("%s/api/v1/user", c.base) + err := c.get(uri, out) + return out, err +} + +// Token returns an oauth2 bearer token for the currently +// authenticated user. +func (c *HTTPClient) Token() (*types.Token, error) { + out := new(types.Token) + uri := fmt.Sprintf("%s/api/v1/user/token", c.base) + err := c.post(uri, nil, out) + return out, err +} + +// User returns a user by ID or email. +func (c *HTTPClient) User(key string) (*types.User, error) { + out := new(types.User) + uri := fmt.Sprintf("%s/api/v1/users/%s", c.base, key) + err := c.get(uri, out) + return out, err +} + +// UserList returns a list of all registered users. +func (c *HTTPClient) UserList(params types.Params) ([]*types.User, error) { + out := []*types.User{} + uri := fmt.Sprintf("%s/api/v1/users?page=%d&per_page=%d", c.base, params.Page, params.Size) + err := c.get(uri, &out) + return out, err +} + +// UserCreate creates a new user account. +func (c *HTTPClient) UserCreate(user *types.User) (*types.User, error) { + out := new(types.User) + uri := fmt.Sprintf("%s/api/v1/users", c.base) + err := c.post(uri, user, out) + return out, err +} + +// UserUpdate updates a user account by ID or email. +func (c *HTTPClient) UserUpdate(key string, user *types.UserInput) (*types.User, error) { + out := new(types.User) + uri := fmt.Sprintf("%s/api/v1/users/%s", c.base, key) + err := c.patch(uri, user, out) + return out, err +} + +// UserDelete deletes a user account by ID or email. +func (c *HTTPClient) UserDelete(key string) error { + uri := fmt.Sprintf("%s/api/v1/users/%s", c.base, key) + err := c.delete(uri) + return err +} + +// +// Pipeline endpoints +// + +// +// Pipeline endpoints +// + +// Pipeline returns a pipeline by slug. +func (c *HTTPClient) Pipeline(slug string) (*types.Pipeline, error) { + out := new(types.Pipeline) + uri := fmt.Sprintf("%s/api/v1/pipelines/%s", c.base, slug) + err := c.get(uri, out) + return out, err +} + +// PipelineList returns a list of all pipelines. +func (c *HTTPClient) PipelineList(params types.Params) ([]*types.Pipeline, error) { + out := []*types.Pipeline{} + uri := fmt.Sprintf("%s/api/v1/pipelines?page=%dper_page=%d", c.base, params.Page, params.Size) + err := c.get(uri, &out) + return out, err +} + +// PipelineCreate creates a new pipeline. +func (c *HTTPClient) PipelineCreate(pipeline *types.Pipeline) (*types.Pipeline, error) { + out := new(types.Pipeline) + uri := fmt.Sprintf("%s/api/v1/pipelines", c.base) + err := c.post(uri, pipeline, out) + return out, err +} + +// PipelineUpdate updates a pipeline. +func (c *HTTPClient) PipelineUpdate(key string, user *types.PipelineInput) (*types.Pipeline, error) { + out := new(types.Pipeline) + uri := fmt.Sprintf("%s/api/v1/pipelines/%s", c.base, key) + err := c.patch(uri, user, out) + return out, err +} + +// PipelineDelete deletes a pipeline. +func (c *HTTPClient) PipelineDelete(key string) error { + uri := fmt.Sprintf("%s/api/v1/pipelines/%s", c.base, key) + err := c.delete(uri) + return err +} + +// +// Execution endpoints +// + +// Execution returns a execution by ID. +func (c *HTTPClient) Execution(pipeline, slug string) (*types.Execution, error) { + out := new(types.Execution) + uri := fmt.Sprintf("%s/api/v1/pipelines/%s/executions/%s", c.base, pipeline, slug) + err := c.get(uri, out) + return out, err +} + +// ExecutionList returns a list of all executions by pipeline id. +func (c *HTTPClient) ExecutionList(pipeline string, params types.Params) ([]*types.Execution, error) { + out := []*types.Execution{} + uri := fmt.Sprintf("%s/api/v1/pipelines/%s/executions?page=%dper_page=%d", c.base, pipeline, params.Page, params.Size) + err := c.get(uri, &out) + return out, err +} + +// ExecutionCreate creates a new execution. +func (c *HTTPClient) ExecutionCreate(pipeline string, execution *types.Execution) (*types.Execution, error) { + out := new(types.Execution) + uri := fmt.Sprintf("%s/api/v1/pipelines/%s/executions", c.base, pipeline) + err := c.post(uri, execution, out) + return out, err +} + +// ExecutionUpdate updates a execution. +func (c *HTTPClient) ExecutionUpdate(pipeline, slug string, execution *types.ExecutionInput) (*types.Execution, error) { + out := new(types.Execution) + uri := fmt.Sprintf("%s/api/v1/pipelines/%s/executions/%s", c.base, pipeline, slug) + err := c.patch(uri, execution, out) + return out, err +} + +// ExecutionDelete deletes a execution. +func (c *HTTPClient) ExecutionDelete(pipeline, slug string) error { + uri := fmt.Sprintf("%s/api/v1/pipelines/%s/executions/%s", c.base, pipeline, slug) + err := c.delete(uri) + return err +} + +// +// http request helper functions +// + +// helper function for making an http GET request. +func (c *HTTPClient) get(rawurl string, out interface{}) error { + return c.do(rawurl, "GET", nil, out) +} + +// helper function for making an http POST request. +func (c *HTTPClient) post(rawurl string, in, out interface{}) error { + return c.do(rawurl, "POST", in, out) +} + +// helper function for making an http PUT request. +func (c *HTTPClient) put(rawurl string, in, out interface{}) error { + return c.do(rawurl, "PUT", in, out) +} + +// helper function for making an http PATCH request. +func (c *HTTPClient) patch(rawurl string, in, out interface{}) error { + return c.do(rawurl, "PATCH", in, out) +} + +// helper function for making an http DELETE request. +func (c *HTTPClient) delete(rawurl string) error { + return c.do(rawurl, "DELETE", nil, nil) +} + +// helper function to make an http request +func (c *HTTPClient) do(rawurl, method string, in, out interface{}) error { + // executes the http request and returns the body as + // and io.ReadCloser + body, err := c.stream(rawurl, method, in, out) + if body != nil { + defer body.Close() + } + if err != nil { + return err + } + + // if a json response is expected, parse and return + // the json response. + if out != nil { + return json.NewDecoder(body).Decode(out) + } + return nil +} + +// helper function to stream an http request +func (c *HTTPClient) stream(rawurl, method string, in, out interface{}) (io.ReadCloser, error) { + uri, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + // if we are posting or putting data, we need to + // write it to the body of the request. + var buf io.ReadWriter + if in != nil { + buf = new(bytes.Buffer) + // if posting form data, encode the form values. + if form, ok := in.(*url.Values); ok { + io.WriteString(buf, form.Encode()) + } else { + if err := json.NewEncoder(buf).Encode(in); err != nil { + return nil, err + } + } + } + + // creates a new http request. + req, err := http.NewRequest(method, uri.String(), buf) + if err != nil { + return nil, err + } + if in != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + if _, ok := in.(*url.Values); ok { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + // include the client version information in the + // http accept header for debugging purposes. + req.Header.Set("Accept", "application/json;version="+version.Version.String()) + + // send the http request. + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + if c.debug { + dump, _ := httputil.DumpResponse(resp, true) + fmt.Println(method, rawurl) + fmt.Println(string(dump)) + } + if resp.StatusCode > 299 { + defer resp.Body.Close() + err := new(remoteError) + json.NewDecoder(resp.Body).Decode(err) + return nil, err + } + return resp.Body, nil +} diff --git a/client/interface.go b/client/interface.go new file mode 100644 index 000000000..ec33e4f33 --- /dev/null +++ b/client/interface.go @@ -0,0 +1,79 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package client + +import "github.com/bradrydzewski/my-app/types" + +// Client to access the remote APIs. +type Client interface { + // Login authenticates the user and returns a JWT token. + Login(username, password string) (*types.Token, error) + + // Register registers a new user and returns a JWT token. + Register(username, password string) (*types.Token, error) + + // Self returns the currently authenticated user. + Self() (*types.User, error) + + // Token returns an oauth2 bearer token for the currently + // authenticated user. + Token() (*types.Token, error) + + // User returns a user by ID or email. + User(key string) (*types.User, error) + + // UserList returns a list of all registered users. + UserList(params types.Params) ([]*types.User, error) + + // UserCreate creates a new user account. + UserCreate(user *types.User) (*types.User, error) + + // UserUpdate updates a user account by ID or email. + UserUpdate(key string, input *types.UserInput) (*types.User, error) + + // UserDelete deletes a user account by ID or email. + UserDelete(key string) error + + // Pipeline returns a pipeline by slug. + Pipeline(slug string) (*types.Pipeline, error) + + // PipelineList returns a list of all pipelines. + PipelineList(params types.Params) ([]*types.Pipeline, error) + + // PipelineCreate creates a new pipeline. + PipelineCreate(user *types.Pipeline) (*types.Pipeline, error) + + // PipelineUpdate updates a pipeline. + PipelineUpdate(slug string, input *types.PipelineInput) (*types.Pipeline, error) + + // PipelineDelete deletes a pipeline. + PipelineDelete(slug string) error + + // Execution returns a execution by pipeline and slug. + Execution(pipeline, slug string) (*types.Execution, error) + + // ExecutionList returns a list of all executions by pipeline slug. + ExecutionList(pipeline string, params types.Params) ([]*types.Execution, error) + + // ExecutionCreate creates a new execution. + ExecutionCreate(pipeline string, execution *types.Execution) (*types.Execution, error) + + // ExecutionUpdate updates a execution. + ExecutionUpdate(pipeline, slug string, input *types.ExecutionInput) (*types.Execution, error) + + // ExecutionDelete deletes a execution. + ExecutionDelete(pipeline, slug string) error +} + +// remoteError store the error payload returned +// fro the remote API. +type remoteError struct { + Message string `json:"message"` +} + +// Error returns the error message. +func (e *remoteError) Error() string { + return e.Message +} diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 000000000..ce0ee9b5f --- /dev/null +++ b/contrib/README.md @@ -0,0 +1 @@ +The contrib directory contains scripts, images, and other helpful things which are not part of the core docker distribution. Please note that they could be out of date, since they do not receive the same attention as the rest of the repository. \ No newline at end of file diff --git a/contrib/kubernetes/spec.yml b/contrib/kubernetes/spec.yml new file mode 100644 index 000000000..3968966e0 --- /dev/null +++ b/contrib/kubernetes/spec.yml @@ -0,0 +1,58 @@ +apiVersion: v1 +items: + +- apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app: my-app + name: my-app + spec: + replicas: 1 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - image: gcr.io/XXXXX-XXXXXXX/my-app + imagePullPolicy: Never + name: my-app + +- apiVersion: v1 + kind: Service + metadata: + labels: + app: my-app + name: my-app + spec: + ports: + - port: 3000 + protocol: TCP + targetPort: 3000 + selector: + app: my-app + +- apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + name: my-app + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + kubernetes.io/ingress.class: "nginx" + spec: + rules: + - http: + paths: + - path: /pm/(.*) + backend: + serviceName: my-app + servicePort: 3000 + +kind: List +metadata: + resourceVersion: "" + selfLink: "" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..3903a2ef7 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3 as alpine +RUN apk add -U --no-cache ca-certificates + +FROM us.gcr.io/platform-205701/busybox:safe +EXPOSE 80 +EXPOSE 443 + +COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +ENV GODEBUG netdns=go +ADD release/linux/amd64/my-app /bin/ +ENTRYPOINT ["/bin/my-app", "server"] diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine new file mode 100644 index 000000000..3e56fce68 --- /dev/null +++ b/docker/Dockerfile.alpine @@ -0,0 +1,12 @@ +FROM alpine:3 as alpine +RUN apk add -U --no-cache ca-certificates + +FROM alpine:3 +EXPOSE 80 +EXPOSE 443 + +COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +ENV GODEBUG netdns=go +ADD release/linux/amd64/my-app /bin/ +ENTRYPOINT ["/bin/my-app", "server"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..839d3fabb --- /dev/null +++ b/go.mod @@ -0,0 +1,51 @@ +module github.com/bradrydzewski/my-app + +go 1.17 + +require ( + github.com/Masterminds/squirrel v1.5.1 + github.com/adrg/xdg v0.3.2 + github.com/coreos/go-semver v0.3.0 + github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/drone/funcmap v0.0.0-20190918184546-d4ef6e88376d + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.0 + github.com/golang/mock v1.5.0 + github.com/google/go-cmp v0.5.5 + github.com/google/wire v0.5.0 + github.com/gosimple/slug v1.11.2 + github.com/gotidy/ptr v1.3.0 + github.com/jmoiron/sqlx v1.3.1 + github.com/joho/godotenv v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/lib/pq v1.10.0 + github.com/maragudk/migrate v0.4.1 + github.com/mattn/go-isatty v0.0.12 + github.com/mattn/go-sqlite3 v1.14.10-0.20211026011849-85436841b33e + github.com/rs/zerolog v1.26.0 + github.com/swaggest/openapi-go v0.2.13 + github.com/swaggest/swgui v1.4.2 + github.com/unrolled/secure v1.0.8 + golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + gopkg.in/alecthomas/kingpin.v2 v2.2.6 +) + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/rs/xid v1.3.0 // indirect + github.com/swaggest/jsonschema-go v0.3.24 // indirect + github.com/swaggest/refl v1.0.1 // indirect + github.com/vearutop/statigz v1.1.5 // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..8d7197ea3 --- /dev/null +++ b/go.sum @@ -0,0 +1,632 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/squirrel v1.5.1 h1:kWAKlLLJFxZG7N2E0mBMNWVp5AuUX+JUrnhFN74Eg+w= +github.com/Masterminds/squirrel v1.5.1/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/adrg/xdg v0.3.2 h1:GUSGQ5pHdev83AYhDSS1A/CX+0JIsxbiWtow2DSA+RU= +github.com/adrg/xdg v0.3.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM= +github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bool64/dev v0.1.17/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= +github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= +github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= +github.com/bool64/dev v0.1.42 h1:Ps0IvNNf/v1MlIXt8Q5YKcKjYsIVLY/fb/5BmA7gepg= +github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= +github.com/bool64/shared v0.1.3 h1:gj7XZPYa1flQsCg3q9AIju+W2A1jaexK0fdFu2XtaG0= +github.com/bool64/shared v0.1.3/go.mod h1:RF1p1Oi29ofgOvinBpetbF5mceOUP3kpMkvLbWOmtm0= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/drone/funcmap v0.0.0-20190918184546-d4ef6e88376d h1:/IO7UVVu191Jc0DajV4cDVoO+91cuppvgxg2MZl+AXI= +github.com/drone/funcmap v0.0.0-20190918184546-d4ef6e88376d/go.mod h1:Hph0/pT6ZxbujnE1Z6/08p5I0XXuOsppqF6NQlGOK0E= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE= +github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gosimple/slug v1.11.2 h1:MxFR0TmQ/qz0KvIrBbf4phu+G0RBgpwxOn6jPKFKFOw= +github.com/gosimple/slug v1.11.2/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/gotidy/ptr v1.3.0 h1:5wdrH1G8X4txy6fbWWRznr7k974wMWtePWP3p6s1API= +github.com/gotidy/ptr v1.3.0/go.mod h1:vpltyHhOZE+NGXUiwpVl3wV9AGEBlxhdnaimPDxRLxg= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= +github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= +github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.0 h1:h2yg3kjIyAGSZKDijYn1/gXHlYLCwl9ZjEh2PU0yVxE= +github.com/jackc/pgproto3/v2 v2.1.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs= +github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI= +github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/maragudk/migrate v0.4.1 h1:oAY8bCyaHIreLj3ar9b6cf7PSqOZsCkKXHU8Yn1bkb4= +github.com/maragudk/migrate v0.4.1/go.mod h1:vhmL4s+Xz75KU6DPZWRfqb45YyqjYQfcXliA1DsYzvY= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.10-0.20211026011849-85436841b33e h1:aF8T4W7RUsJjsbSVHyxjqt7aWrGpkCN8m6m8D38SU8Q= +github.com/mattn/go-sqlite3 v1.14.10-0.20211026011849-85436841b33e/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= +github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggest/assertjson v1.6.8 h1:1O/9UI5M+2OJI7BeEWKGj0wTvpRXZt5FkOJ4nRkY4rA= +github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M= +github.com/swaggest/jsonschema-go v0.3.24 h1:vVtFhWQWT9sgsvgMdh0n3O1F0+TnhOWz4+lbz5p5ChY= +github.com/swaggest/jsonschema-go v0.3.24/go.mod h1:B2ZSqrrlkj21zhywlkh8VnKBmuqUwDv3dLQoPjgHk7M= +github.com/swaggest/openapi-go v0.2.13 h1:2u3Im5S6BX6GFqux2I035FSXWn6jgWsXY8SNhc0jHE4= +github.com/swaggest/openapi-go v0.2.13/go.mod h1:Y3+sBULNSPwoUbExcY+0AwjWX0oxFspaBGha7tx/DWk= +github.com/swaggest/refl v1.0.0/go.mod h1:acYd5x8NNxivp+ZHdRZKJYz66n/qjo3Q9Sa/jAivljQ= +github.com/swaggest/refl v1.0.1 h1:YQHb7Ic6EMpdUpxQmTWmf/O4IWN6iIErxJNWA7LwyyM= +github.com/swaggest/refl v1.0.1/go.mod h1:dnx+n9YaI0o+FH+OR2tJZWLABBVIPs9qc4VY9UdrhLE= +github.com/swaggest/swgui v1.4.2 h1:6AT8ICO0+t6WpbIFsACf5vBmviVX0sqspNbZLoe6vgw= +github.com/swaggest/swgui v1.4.2/go.mod h1:xWDsT2h8obEoGHzX/a6FRClUOS8NvkICyInhi7s3fN8= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/unrolled/secure v1.0.8 h1:JaMvKbe4CRt8oyxVXn+xY+6jlqd7pyJNSVkmsBxxQsM= +github.com/unrolled/secure v1.0.8/go.mod h1:fO+mEan+FLB0CdEnHf6Q4ZZVNqG+5fuLFnP8p0BXDPI= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/vearutop/statigz v1.1.5 h1:qWvRgXFsseWVTFCkIvwHQPpaLNf9WI0+dDJE7I9432o= +github.com/vearutop/statigz v1.1.5/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 000000000..4d2fe063d --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package api diff --git a/internal/api/handler/account/login.go b/internal/api/handler/account/login.go new file mode 100644 index 000000000..3a6dbb23f --- /dev/null +++ b/internal/api/handler/account/login.go @@ -0,0 +1,75 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package account + +import ( + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/token" + "github.com/bradrydzewski/my-app/types" + + "github.com/rs/zerolog/hlog" + "golang.org/x/crypto/bcrypt" +) + +// HandleLogin returns an http.HandlerFunc that authenticates +// the user and returns an authentication token on success. +func HandleLogin(users store.UserStore, system store.SystemStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + username := r.FormValue("username") + password := r.FormValue("password") + user, err := users.FindEmail(ctx, username) + if err != nil { + render.NotFoundf(w, "Invalid email or password") + log.Debug().Err(err). + Str("user", username). + Msg("cannot find user") + return + } + + err = bcrypt.CompareHashAndPassword( + []byte(user.Password), + []byte(password), + ) + if err != nil { + render.NotFoundf(w, "Invalid email or password") + log.Debug().Err(err). + Str("user", username). + Msg("invalid password") + return + } + + expires := time.Now().Add(system.Config(ctx).Token.Expire) + token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) + if err != nil { + render.InternalErrorf(w, "Failed to create session") + log.Debug().Err(err). + Str("user", username). + Msg("failed to generate token") + return + } + + // return the token if the with_user boolean + // query parameter is set to true. + if r.FormValue("return_user") == "true" { + render.JSON(w, &types.UserToken{ + User: user, + Token: &types.Token{ + Value: token_, + Expires: expires.UTC(), + }, + }, 200) + } else { + // else return the token only. + render.JSON(w, &types.Token{Value: token_}, 200) + } + } +} diff --git a/internal/api/handler/account/login_test.go b/internal/api/handler/account/login_test.go new file mode 100644 index 000000000..0b1057461 --- /dev/null +++ b/internal/api/handler/account/login_test.go @@ -0,0 +1,23 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package account + +import "testing" + +func TestLogin(t *testing.T) { + t.Skip() +} + +func TestLogin_NotFound(t *testing.T) { + t.Skip() +} + +func TestLogin_BcryptError(t *testing.T) { + t.Skip() +} + +func TestLogin_TokenError(t *testing.T) { + t.Skip() +} diff --git a/internal/api/handler/account/register.go b/internal/api/handler/account/register.go new file mode 100644 index 000000000..49886b44d --- /dev/null +++ b/internal/api/handler/account/register.go @@ -0,0 +1,103 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package account + +import ( + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/token" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + + "github.com/dchest/uniuri" + "github.com/rs/zerolog/hlog" + "golang.org/x/crypto/bcrypt" +) + +// HandleRegister returns an http.HandlerFunc that processes an http.Request +// to register the named user account with the system. +func HandleRegister(users store.UserStore, system store.SystemStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + username := r.FormValue("username") + password := r.FormValue("password") + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + render.InternalError(w, err) + log.Debug().Err(err). + Str("email", username). + Msg("cannot hash password") + return + } + + user := &types.User{ + Name: username, + Email: username, + Password: string(hash), + Salt: uniuri.NewLen(uniuri.UUIDLen), + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + if ok, err := check.User(user); !ok { + render.BadRequest(w, err) + log.Debug().Err(err). + Str("email", username). + Msg("invalid user input") + return + } + + if err := users.Create(ctx, user); err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Str("email", username). + Msg("cannot create user") + return + } + + // if the registered user is the first user of the system, + // assume they are the system administrator and grant the + // user system admin access. + if user.ID == 1 { + user.Admin = true + if err := users.Update(ctx, user); err != nil { + log.Error().Err(err). + Str("email", username). + Msg("cannot enable admin user") + } + } + + expires := time.Now().Add(system.Config(ctx).Token.Expire) + token_, err := token.GenerateExp(user, expires.Unix(), user.Salt) + if err != nil { + render.InternalErrorf(w, "Failed to create session") + log.Error().Err(err). + Str("email", username). + Msg("failed to generate token") + return + } + + // return the token if the with_user boolean + // query parameter is set to true. + if r.FormValue("return_user") == "true" { + render.JSON(w, &types.UserToken{ + User: user, + Token: &types.Token{ + Value: token_, + Expires: expires.UTC(), + }, + }, 200) + } else { + // else return the token only. + render.JSON(w, &types.Token{Value: token_}, 200) + } + } +} diff --git a/internal/api/handler/account/register_test.go b/internal/api/handler/account/register_test.go new file mode 100644 index 000000000..440844de5 --- /dev/null +++ b/internal/api/handler/account/register_test.go @@ -0,0 +1,27 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package account + +import "testing" + +func TestRegiser(t *testing.T) { + t.Skip() +} + +func TestRegiserAdmin(t *testing.T) { + t.Skip() +} + +func TestRegiser_CreateError(t *testing.T) { + t.Skip() +} + +func TestRegiser_BcryptError(t *testing.T) { + t.Skip() +} + +func TestRegiser_TokenError(t *testing.T) { + t.Skip() +} diff --git a/internal/api/handler/executions/create.go b/internal/api/handler/executions/create.go new file mode 100644 index 000000000..b5573df4e --- /dev/null +++ b/internal/api/handler/executions/create.go @@ -0,0 +1,96 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + "github.com/go-chi/chi" + "github.com/gosimple/slug" + "github.com/gotidy/ptr" + "github.com/rs/zerolog/hlog" +) + +// HandleCreate returns an http.HandlerFunc that creates +// the object and persists to the datastore. +func HandleCreate(pipelines store.PipelineStore, executions store.ExecutionStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + param = chi.URLParam(r, "pipeline") + ) + + pipeline, err := pipelines.FindSlug(ctx, param) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", param). + Msg("pipeline not found") + return + } + + sublog := log.With(). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Logger() + + in := new(types.ExecutionInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + sublog.Debug().Err(err). + Msg("cannot unmarshal json request") + return + } + + execution := &types.Execution{ + Pipeline: pipeline.ID, + Slug: ptr.ToString(in.Slug), + Name: ptr.ToString(in.Name), + Desc: ptr.ToString(in.Desc), + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // if the slug is empty we can derrive + // the slug from the name. + if execution.Slug == "" { + execution.Slug = slug.Make(execution.Name) + } + + // if the name is empty we can derrive + // the name from the slug. + if execution.Name == "" { + execution.Name = execution.Slug + } + + if ok, err := check.Execution(execution); !ok { + render.BadRequest(w, err) + sublog.Debug().Err(err). + Int64("execution_id", execution.ID). + Str("execution_slug", execution.Slug). + Msg("cannot validate execution") + return + } + + err = executions.Create(ctx, execution) + if err != nil { + render.InternalError(w, err) + sublog.Error().Err(err). + Int64("execution_id", execution.ID). + Str("execution_slug", execution.Slug). + Msg("cannot create execution") + } else { + render.JSON(w, execution, 200) + } + } +} diff --git a/internal/api/handler/executions/create_test.go b/internal/api/handler/executions/create_test.go new file mode 100644 index 000000000..f1d9c00cf --- /dev/null +++ b/internal/api/handler/executions/create_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions diff --git a/internal/api/handler/executions/delete.go b/internal/api/handler/executions/delete.go new file mode 100644 index 000000000..276a9fe27 --- /dev/null +++ b/internal/api/handler/executions/delete.go @@ -0,0 +1,61 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + + "github.com/go-chi/chi" + "github.com/rs/zerolog/hlog" +) + +// HandleDelete returns an http.HandlerFunc that deletes +// the object from the datastore. +func HandleDelete(pipelines store.PipelineStore, executions store.ExecutionStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + pipelineParam = chi.URLParam(r, "pipeline") + executionParam = chi.URLParam(r, "execution") + ) + + pipeline, err := pipelines.FindSlug(ctx, pipelineParam) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", pipelineParam). + Msg("pipeline not found") + return + } + + execution, err := executions.FindSlug(ctx, pipeline.ID, executionParam) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Str("execution_slug", executionParam). + Msg("execution not found") + return + } + + err = executions.Delete(ctx, execution) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Int64("execution_id", execution.ID). + Str("execution_slug", execution.Slug). + Msg("cannot delete execution") + } else { + w.WriteHeader(http.StatusNoContent) + } + } +} diff --git a/internal/api/handler/executions/delete_test.go b/internal/api/handler/executions/delete_test.go new file mode 100644 index 000000000..f1d9c00cf --- /dev/null +++ b/internal/api/handler/executions/delete_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions diff --git a/internal/api/handler/executions/find.go b/internal/api/handler/executions/find.go new file mode 100644 index 000000000..88a6fada2 --- /dev/null +++ b/internal/api/handler/executions/find.go @@ -0,0 +1,50 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + + "github.com/go-chi/chi" + "github.com/rs/zerolog/hlog" +) + +// HandleFind returns an http.HandlerFunc that writes the +// json-encoded execution details to the response body. +func HandleFind(pipelines store.PipelineStore, executions store.ExecutionStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + pipelineParam = chi.URLParam(r, "pipeline") + executionParam = chi.URLParam(r, "execution") + ) + + pipeline, err := pipelines.FindSlug(ctx, pipelineParam) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", pipelineParam). + Msg("pipeline not found") + return + } + + execution, err := executions.FindSlug(ctx, pipeline.ID, executionParam) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Str("execution_slug", executionParam). + Msg("execution not found") + return + } + + render.JSON(w, execution, 200) + } +} diff --git a/internal/api/handler/executions/find_test.go b/internal/api/handler/executions/find_test.go new file mode 100644 index 000000000..f1d9c00cf --- /dev/null +++ b/internal/api/handler/executions/find_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions diff --git a/internal/api/handler/executions/list.go b/internal/api/handler/executions/list.go new file mode 100644 index 000000000..cac038f75 --- /dev/null +++ b/internal/api/handler/executions/list.go @@ -0,0 +1,55 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/go-chi/chi" + "github.com/rs/zerolog/hlog" +) + +// HandleList returns an http.HandlerFunc that writes a json-encoded +// list of objects to the response body. +func HandleList(pipelines store.PipelineStore, executions store.ExecutionStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + slug = chi.URLParam(r, "pipeline") + page = request.ParsePage(r) + size = request.ParseSize(r) + ) + + pipeline, err := pipelines.FindSlug(ctx, slug) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", slug). + Msg("pipeline not found") + return + } + + executions, err := executions.List(ctx, pipeline.ID, types.Params{ + Size: size, + Page: page, + }) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot retrieve list") + } else { + render.Pagination(r, w, page, size, 0) + render.JSON(w, executions, 200) + } + } +} diff --git a/internal/api/handler/executions/list_test.go b/internal/api/handler/executions/list_test.go new file mode 100644 index 000000000..f1d9c00cf --- /dev/null +++ b/internal/api/handler/executions/list_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions diff --git a/internal/api/handler/executions/update.go b/internal/api/handler/executions/update.go new file mode 100644 index 000000000..7d240a0cd --- /dev/null +++ b/internal/api/handler/executions/update.go @@ -0,0 +1,95 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + + "github.com/go-chi/chi" + "github.com/gotidy/ptr" + "github.com/rs/zerolog/hlog" +) + +// HandleUpdate returns an http.HandlerFunc that processes http +// requests to update the object details. +func HandleUpdate(pipelines store.PipelineStore, executions store.ExecutionStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + pipelineParam = chi.URLParam(r, "pipeline") + executionParam = chi.URLParam(r, "execution") + ) + + pipeline, err := pipelines.FindSlug(ctx, pipelineParam) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", pipelineParam). + Msg("pipeline not found") + return + } + + sublog := log.With(). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Logger() + + execution, err := executions.FindSlug(ctx, pipeline.ID, executionParam) + if err != nil { + render.NotFound(w, err) + sublog.Debug().Err(err). + Str("execution_slug", executionParam). + Msg("execution not found") + return + } + + sublog = sublog.With(). + Str("execution_slug", execution.Slug). + Int64("execution_id", execution.ID). + Logger() + + in := new(types.ExecutionInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + sublog.Debug().Err(err). + Msg("cannot unmarshal json request") + return + } + + if in.Name != nil { + execution.Name = ptr.ToString(in.Name) + } + if in.Desc != nil { + execution.Desc = ptr.ToString(in.Desc) + } + + if ok, err := check.Execution(execution); !ok { + render.BadRequest(w, err) + sublog.Debug().Err(err). + Msg("cannot validate execution") + return + } + + execution.Updated = time.Now().UnixMilli() + + err = executions.Update(ctx, execution) + if err != nil { + render.InternalError(w, err) + sublog.Error().Err(err). + Msg("cannot update execution") + } else { + render.JSON(w, execution, 200) + } + } +} diff --git a/internal/api/handler/executions/update_test.go b/internal/api/handler/executions/update_test.go new file mode 100644 index 000000000..f1d9c00cf --- /dev/null +++ b/internal/api/handler/executions/update_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package executions diff --git a/internal/api/handler/pipelines/create.go b/internal/api/handler/pipelines/create.go new file mode 100644 index 000000000..476a5703f --- /dev/null +++ b/internal/api/handler/pipelines/create.go @@ -0,0 +1,80 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + + "github.com/dchest/uniuri" + "github.com/gosimple/slug" + "github.com/gotidy/ptr" + "github.com/rs/zerolog/hlog" +) + +// HandleCreate returns an http.HandlerFunc that creates +// a new pipeline. +func HandleCreate(pipelines store.PipelineStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + in := new(types.PipelineInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + log.Debug().Err(err). + Msg("cannot unmarshal json request") + return + } + + pipeline := &types.Pipeline{ + Slug: ptr.ToString(in.Slug), + Name: ptr.ToString(in.Name), + Desc: ptr.ToString(in.Desc), + Token: uniuri.NewLen(uniuri.UUIDLen), + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + // if the slug is empty we can derrive + // the slug from the pipeline name. + if pipeline.Slug == "" { + pipeline.Slug = slug.Make(pipeline.Name) + } + + // if the name is empty we can derrive + // the name from the pipeline slug. + if pipeline.Name == "" { + pipeline.Name = pipeline.Slug + } + + if ok, err := check.Pipeline(pipeline); !ok { + render.BadRequest(w, err) + log.Debug().Err(err). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot create pipeline") + return + } + + err = pipelines.Create(ctx, pipeline) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Str("pipeline_name", pipeline.Name). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot create pipeline") + return + } + + render.JSON(w, pipeline, 200) + } +} diff --git a/internal/api/handler/pipelines/create_test.go b/internal/api/handler/pipelines/create_test.go new file mode 100644 index 000000000..3b9c776b3 --- /dev/null +++ b/internal/api/handler/pipelines/create_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines diff --git a/internal/api/handler/pipelines/delete.go b/internal/api/handler/pipelines/delete.go new file mode 100644 index 000000000..c02407494 --- /dev/null +++ b/internal/api/handler/pipelines/delete.go @@ -0,0 +1,46 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + + "github.com/go-chi/chi" + "github.com/rs/zerolog/hlog" +) + +// HandleDelete returns an http.HandlerFunc that deletes +// the object from the datastore. +func HandleDelete(pipelines store.PipelineStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "pipeline") + + pipeline, err := pipelines.FindSlug(ctx, id) + if err != nil { + render.NotFound(w, err) + hlog.FromRequest(r). + Debug().Err(err). + Str("pipeline_slug", id). + Msg("pipeline not found") + return + } + + err = pipelines.Delete(ctx, pipeline) + if err != nil { + render.InternalError(w, err) + hlog.FromRequest(r). + Error().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot delete pipeline") + } else { + w.WriteHeader(http.StatusNoContent) + } + } +} diff --git a/internal/api/handler/pipelines/delete_test.go b/internal/api/handler/pipelines/delete_test.go new file mode 100644 index 000000000..3b9c776b3 --- /dev/null +++ b/internal/api/handler/pipelines/delete_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines diff --git a/internal/api/handler/pipelines/find.go b/internal/api/handler/pipelines/find.go new file mode 100644 index 000000000..4dab208bc --- /dev/null +++ b/internal/api/handler/pipelines/find.go @@ -0,0 +1,51 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/rs/zerolog/hlog" + + "github.com/go-chi/chi" +) + +type pipelineToken struct { + *types.Pipeline + Token string `json:"token"` +} + +// HandleFind returns an http.HandlerFunc that writes the +// json-encoded pipeline details to the response body. +func HandleFind(pipelines store.PipelineStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "pipeline") + + pipeline, err := pipelines.FindSlug(ctx, id) + if err != nil { + render.NotFound(w, err) + hlog.FromRequest(r). + Debug().Err(err). + Str("pipeline_slug", id). + Msg("pipeline not found") + return + } + + // if the caller requests the pipeline details without + // the token, return the token object as-is. + if r.FormValue("token") != "true" { + render.JSON(w, pipeline, 200) + return + } + + // if the caller requests the pipeline details with + // the token then it can be safely included. + render.JSON(w, &pipelineToken{pipeline, pipeline.Token}, 200) + } +} diff --git a/internal/api/handler/pipelines/find_test.go b/internal/api/handler/pipelines/find_test.go new file mode 100644 index 000000000..3b9c776b3 --- /dev/null +++ b/internal/api/handler/pipelines/find_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines diff --git a/internal/api/handler/pipelines/list.go b/internal/api/handler/pipelines/list.go new file mode 100644 index 000000000..46bb8b0fa --- /dev/null +++ b/internal/api/handler/pipelines/list.go @@ -0,0 +1,42 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/rs/zerolog/log" +) + +// HandleList returns an http.HandlerFunc that writes a json-encoded +// list of pipelines to the response body. +func HandleList(pipelines store.PipelineStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + page = request.ParsePage(r) + size = request.ParseSize(r) + ctx = r.Context() + ) + + viewer, _ := request.UserFrom(ctx) + list, err := pipelines.List(ctx, viewer.ID, types.Params{ + Page: page, + Size: size, + }) + if err != nil { + render.InternalError(w, err) + log.Ctx(ctx).Error(). + Err(err).Msg("cannot list pipelines") + } else { + render.Pagination(r, w, page, size, 0) + render.JSON(w, list, 200) + } + } +} diff --git a/internal/api/handler/pipelines/list_test.go b/internal/api/handler/pipelines/list_test.go new file mode 100644 index 000000000..4d6a34c1b --- /dev/null +++ b/internal/api/handler/pipelines/list_test.go @@ -0,0 +1,87 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/mocks" + "github.com/bradrydzewski/my-app/types" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" +) + +func TestHandleList(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + mockUser := &types.User{ + ID: 1, + Email: "octocat@github.com", + } + + mockList := []*types.Pipeline{ + { + Name: "test", + Desc: "desc", + }, + } + + projs := mocks.NewMockPipelineStore(controller) + projs.EXPECT().List(gomock.Any(), mockUser.ID, gomock.Any()).Return(mockList, nil) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r = r.WithContext( + request.WithUser(r.Context(), mockUser), + ) + + HandleList(projs)(w, r) + if got, want := w.Code, http.StatusOK; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := []*types.Pipeline{}, mockList + json.NewDecoder(w.Body).Decode(&got) + if diff := cmp.Diff(got, want); len(diff) > 0 { + t.Errorf(diff) + } +} + +func TestListErr(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + mockUser := &types.User{ + ID: 1, + Email: "octocat@github.com", + } + + projs := mocks.NewMockPipelineStore(controller) + projs.EXPECT().List(gomock.Any(), mockUser.ID, gomock.Any()).Return(nil, render.ErrNotFound) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r = r.WithContext( + request.WithUser(r.Context(), mockUser), + ) + + HandleList(projs)(w, r) + if got, want := w.Code, http.StatusInternalServerError; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := &render.Error{}, render.ErrNotFound + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) > 0 { + t.Errorf(diff) + } +} diff --git a/internal/api/handler/pipelines/update.go b/internal/api/handler/pipelines/update.go new file mode 100644 index 000000000..090423297 --- /dev/null +++ b/internal/api/handler/pipelines/update.go @@ -0,0 +1,79 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + + "github.com/go-chi/chi" + "github.com/gotidy/ptr" + "github.com/rs/zerolog/hlog" +) + +// HandleUpdate returns an http.HandlerFunc that processes http +// requests to update the pipeline details. +func HandleUpdate(pipelines store.PipelineStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + id := chi.URLParam(r, "pipeline") + + pipeline, err := pipelines.FindSlug(ctx, id) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("pipeline_slug", id). + Msg("pipeline not found") + return + } + + in := new(types.PipelineInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + log.Debug().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot unmarshal json request") + return + } + + if in.Name != nil { + pipeline.Name = ptr.ToString(in.Name) + } + if in.Desc != nil { + pipeline.Desc = ptr.ToString(in.Desc) + } + + if ok, err := check.Pipeline(pipeline); !ok { + render.BadRequest(w, err) + log.Debug().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot update pipeline") + return + } + + pipeline.Updated = time.Now().UnixMilli() + + err = pipelines.Update(ctx, pipeline) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("pipeline_id", pipeline.ID). + Str("pipeline_slug", pipeline.Slug). + Msg("cannot update the pipeline") + } else { + render.JSON(w, pipeline, 200) + } + } +} diff --git a/internal/api/handler/pipelines/update_test.go b/internal/api/handler/pipelines/update_test.go new file mode 100644 index 000000000..3b9c776b3 --- /dev/null +++ b/internal/api/handler/pipelines/update_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipelines diff --git a/internal/api/handler/projects/find.go b/internal/api/handler/projects/find.go new file mode 100644 index 000000000..16de9e2c9 --- /dev/null +++ b/internal/api/handler/projects/find.go @@ -0,0 +1,32 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package projects + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render/platform" + "github.com/bradrydzewski/my-app/types" +) + +// standalone version of the product uses a single, +// hard-coded project as its default. +var defaultProject = &types.Project{ + Identifier: "default", + Color: "#0063f7", + Desc: "Default Project", + Name: "Default Project", + Modules: []string{}, + Org: "default", + Tags: map[string]string{}, +} + +// HandleFind returns an http.HandlerFunc that writes json-encoded +// project to the http response body. +func HandleFind() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + platform.RenderResource(w, defaultProject, 200) + } +} diff --git a/internal/api/handler/projects/find_test.go b/internal/api/handler/projects/find_test.go new file mode 100644 index 000000000..e2baed957 --- /dev/null +++ b/internal/api/handler/projects/find_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package projects diff --git a/internal/api/handler/projects/list.go b/internal/api/handler/projects/list.go new file mode 100644 index 000000000..941c3b244 --- /dev/null +++ b/internal/api/handler/projects/list.go @@ -0,0 +1,32 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package projects + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render/platform" + "github.com/bradrydzewski/my-app/types" +) + +// standalone version of the product uses a single, +// hard-coded project as its default. +var defaultProjectList = &types.ProjectList{ + Data: []*types.Project{defaultProject}, + Empty: false, + PageIndex: 1, + PageItemCount: 1, + PageSize: 1, + TotalItems: 1, + TotalPages: 1, +} + +// HandleList returns an http.HandlerFunc that writes json-encoded +// project list to the http response body. +func HandleList() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + platform.RenderResource(w, defaultProjectList, 200) + } +} diff --git a/internal/api/handler/projects/list_test.go b/internal/api/handler/projects/list_test.go new file mode 100644 index 000000000..e2baed957 --- /dev/null +++ b/internal/api/handler/projects/list_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package projects diff --git a/internal/api/handler/system/health.go b/internal/api/handler/system/health.go new file mode 100644 index 000000000..955ef9da3 --- /dev/null +++ b/internal/api/handler/system/health.go @@ -0,0 +1,13 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package system + +import "net/http" + +// HandleHealth writes a 200 OK status to the http.Response +// if the server is healthy. +func HandleHealth(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/internal/api/handler/system/health_test.go b/internal/api/handler/system/health_test.go new file mode 100644 index 000000000..65a184384 --- /dev/null +++ b/internal/api/handler/system/health_test.go @@ -0,0 +1,11 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package system + +import "testing" + +func TestHealth(t *testing.T) { + t.Skip() +} diff --git a/internal/api/handler/system/version.go b/internal/api/handler/system/version.go new file mode 100644 index 000000000..3ed55f72e --- /dev/null +++ b/internal/api/handler/system/version.go @@ -0,0 +1,18 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package system + +import ( + "fmt" + "net/http" + + "github.com/bradrydzewski/my-app/version" +) + +// HandleVersion writes the server version number +// to the http.Response body in plain text. +func HandleVersion(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%s", version.Version) +} diff --git a/internal/api/handler/system/version_test.go b/internal/api/handler/system/version_test.go new file mode 100644 index 000000000..254139a40 --- /dev/null +++ b/internal/api/handler/system/version_test.go @@ -0,0 +1,11 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package system + +import "testing" + +func TestVersion(t *testing.T) { + t.Skip() +} diff --git a/internal/api/handler/user/find.go b/internal/api/handler/user/find.go new file mode 100644 index 000000000..7bf6bbb80 --- /dev/null +++ b/internal/api/handler/user/find.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/render/platform" + "github.com/bradrydzewski/my-app/internal/api/request" +) + +// HandleFind returns an http.HandlerFunc that writes json-encoded +// account information to the http response body. +func HandleFind() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + viewer, _ := request.UserFrom(ctx) + render.JSON(w, viewer, 200) + } +} + +// func returns an http.HandlerFunc that writes json-encoded +// account information to the http response body in platform +// format. +func HandleCurrent() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + viewer, _ := request.UserFrom(ctx) + platform.RenderResource(w, viewer, 200) + } +} diff --git a/internal/api/handler/user/find_test.go b/internal/api/handler/user/find_test.go new file mode 100644 index 000000000..27bd1c58c --- /dev/null +++ b/internal/api/handler/user/find_test.go @@ -0,0 +1,40 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/types" + + "github.com/google/go-cmp/cmp" +) + +func TestFind(t *testing.T) { + mockUser := &types.User{ + ID: 1, + Email: "octocat@github.com", + } + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/api/v1/user", nil) + r = r.WithContext( + request.WithUser(r.Context(), mockUser), + ) + + HandleFind()(w, r) + if got, want := w.Code, 200; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := &types.User{}, mockUser + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} diff --git a/internal/api/handler/user/token.go b/internal/api/handler/user/token.go new file mode 100644 index 000000000..5bf7edd82 --- /dev/null +++ b/internal/api/handler/user/token.go @@ -0,0 +1,36 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/token" + "github.com/bradrydzewski/my-app/types" + "github.com/rs/zerolog/hlog" +) + +// HandleToken returns an http.HandlerFunc that generates and +// writes a json-encoded token to the http.Response body. +func HandleToken(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + viewer, _ := request.UserFrom(r.Context()) + + token, err := token.Generate(viewer, viewer.Salt) + if err != nil { + render.InternalErrorf(w, "Failed to generate token") + hlog.FromRequest(r). + Error().Err(err). + Str("user", viewer.Email). + Msg("failed to generate token") + return + } + + render.JSON(w, &types.Token{Value: token}, 200) + } +} diff --git a/internal/api/handler/user/token_test.go b/internal/api/handler/user/token_test.go new file mode 100644 index 000000000..94da737f7 --- /dev/null +++ b/internal/api/handler/user/token_test.go @@ -0,0 +1,49 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/types" + "github.com/dgrijalva/jwt-go" + + "github.com/golang/mock/gomock" +) + +func TestToken(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + mockUser := &types.User{ + ID: 1, + Email: "octocat@github.com", + Salt: "12345", + } + + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/", nil) + r = r.WithContext( + request.WithUser(r.Context(), mockUser), + ) + + HandleToken(nil)(w, r) + if got, want := w.Code, 200; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + result := &types.Token{} + json.NewDecoder(w.Body).Decode(&result) + + _, err := jwt.Parse(result.Value, func(token *jwt.Token) (interface{}, error) { + return []byte(mockUser.Salt), nil + }) + if err != nil { + t.Error(err) + } +} diff --git a/internal/api/handler/user/update.go b/internal/api/handler/user/update.go new file mode 100644 index 000000000..303b99d40 --- /dev/null +++ b/internal/api/handler/user/update.go @@ -0,0 +1,72 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "encoding/json" + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/rs/zerolog/hlog" + + "github.com/gotidy/ptr" + "golang.org/x/crypto/bcrypt" +) + +// GenerateFromPassword returns the bcrypt hash of the +// password at the given cost. +var hashPassword = bcrypt.GenerateFromPassword + +// HandleUpdate returns an http.HandlerFunc that processes an http.Request +// to update the current user account. +func HandleUpdate(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + viewer, _ := request.UserFrom(ctx) + + in := new(types.UserInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + log.Error().Err(err). + Str("email", viewer.Email). + Msg("cannot unmarshal request") + return + } + + if in.Password != nil { + hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) + if err != nil { + render.InternalError(w, err) + log.Debug().Err(err). + Msg("cannot hash password") + return + } + viewer.Password = string(hash) + } + + if in.Name != nil { + viewer.Name = ptr.ToString(in.Name) + } + + if in.Company != nil { + viewer.Company = ptr.ToString(in.Company) + } + + err = users.Update(ctx, viewer) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Str("email", viewer.Email). + Msg("cannot update user") + } else { + render.JSON(w, viewer, 200) + } + } +} diff --git a/internal/api/handler/user/update_test.go b/internal/api/handler/user/update_test.go new file mode 100644 index 000000000..723a5c6c1 --- /dev/null +++ b/internal/api/handler/user/update_test.go @@ -0,0 +1,196 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package user + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/mocks" + "github.com/bradrydzewski/my-app/types" + "golang.org/x/crypto/bcrypt" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/gotidy/ptr" +) + +// mock bcrypt has function returns an error +// when attepting to has the password. +func bcryptHashErrror(password []byte, cost int) ([]byte, error) { + return nil, bcrypt.ErrHashTooShort +} + +// mock bcrypt has function returns a deterministic +// hash value for testing purposes. +func bcryptHashStatic(password []byte, cost int) ([]byte, error) { + return []byte("$2a$10$onMfkmQZtlkOfnZJe7GaiesbPBbXcyB53KyFKllWq829mxlhNoJSi"), nil +} + +func TestUpdate(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + hashPassword = bcryptHashStatic + defer func() { + hashPassword = bcrypt.GenerateFromPassword + }() + + userInput := &types.UserInput{ + Username: ptr.String("octocat@google.com"), + Password: ptr.String("password"), + Company: ptr.String("google"), + } + before := &types.User{ + Email: "octocat@google.com", + Password: "acme", + } + + users := mocks.NewMockUserStore(controller) + users.EXPECT().Update(gomock.Any(), before) + + in := new(bytes.Buffer) + json.NewEncoder(in).Encode(userInput) + w := httptest.NewRecorder() + r := httptest.NewRequest("PATCH", "/api/v1/user", in) + r = r.WithContext( + request.WithUser(r.Context(), before), + ) + + HandleUpdate(users)(w, r) + if got, want := w.Code, 200; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + if got, want := before.Email, "octocat@google.com"; got != want { + t.Errorf("Want user email %v, got %v", want, got) + } + if got, want := before.Password, "$2a$10$onMfkmQZtlkOfnZJe7GaiesbPBbXcyB53KyFKllWq829mxlhNoJSi"; got != want { + t.Errorf("Want user password %v, got %v", want, got) + } + + after := &types.User{ + Email: "octocat@google.com", + Company: "google", + // Password hash is not exposecd to JSON + } + got, want := new(types.User), after + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} + +// the purpose of this unit test is to verify that a +// failure to hash the password will return an internal +// server error. +func TestUpdate_HashError(t *testing.T) { + hashPassword = bcryptHashErrror + defer func() { + hashPassword = bcrypt.GenerateFromPassword + }() + + controller := gomock.NewController(t) + defer controller.Finish() + + userInput := &types.UserInput{ + Username: ptr.String("octocat@github.com"), + Password: ptr.String("password"), + } + user := &types.User{ + Email: "octocat@github.com", + } + + in := new(bytes.Buffer) + json.NewEncoder(in).Encode(userInput) + w := httptest.NewRecorder() + r := httptest.NewRequest("PATCH", "/api/v1/user", in) + r = r.WithContext( + request.WithUser(r.Context(), user), + ) + + HandleUpdate(nil)(w, r) + if got, want := w.Code, 500; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := new(render.Error), &render.Error{Message: bcrypt.ErrHashTooShort.Error()} + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} + +// the purpose of this unit test is to verify that an invalid +// (in this case missing) request body will result in a bad +// request error returned to the client. +func TestUpdate_BadRequest(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + mockUser := &types.User{ + ID: 1, + Email: "octocat@github.com", + } + + in := new(bytes.Buffer) + w := httptest.NewRecorder() + r := httptest.NewRequest("PATCH", "/api/v1/user", in) + r = r.WithContext( + request.WithUser(r.Context(), mockUser), + ) + + HandleUpdate(nil)(w, r) + if got, want := w.Code, 400; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := new(render.Error), &render.Error{Message: "EOF"} + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} + +// the purpose of this unit test is to verify that an error +// updating the database will result in an internal server +// error returned to the client. +func TestUpdate_ServerError(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + + userInput := &types.UserInput{ + Username: ptr.String("octocat@github.com"), + } + user := &types.User{ + Email: "octocat@github.com", + } + + users := mocks.NewMockUserStore(controller) + users.EXPECT().Update(gomock.Any(), user).Return(render.ErrNotFound) + + in := new(bytes.Buffer) + json.NewEncoder(in).Encode(userInput) + w := httptest.NewRecorder() + r := httptest.NewRequest("PATCH", "/api/v1/user", in) + r = r.WithContext( + request.WithUser(r.Context(), user), + ) + + HandleUpdate(users)(w, r) + if got, want := w.Code, 500; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + got, want := new(render.Error), render.ErrNotFound + json.NewDecoder(w.Body).Decode(got) + if diff := cmp.Diff(got, want); len(diff) != 0 { + t.Errorf(diff) + } +} diff --git a/internal/api/handler/users/create.go b/internal/api/handler/users/create.go new file mode 100644 index 000000000..61daaafa3 --- /dev/null +++ b/internal/api/handler/users/create.go @@ -0,0 +1,80 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + "github.com/rs/zerolog/hlog" + "golang.org/x/crypto/bcrypt" + + "github.com/dchest/uniuri" +) + +type userCreateInput struct { + Username string `json:"email"` + Password string `json:"password"` + Admin bool `json:"admin"` +} + +// HandleCreate returns an http.HandlerFunc that processes an http.Request +// to create the named user account in the system. +func HandleCreate(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + in := new(userCreateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequest(w, err) + log.Debug().Err(err). + Msg("cannot unmarshal json request") + return + } + + hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) + if err != nil { + render.InternalError(w, err) + log.Debug().Err(err). + Msg("cannot hash password") + return + } + + user := &types.User{ + Email: in.Username, + Admin: in.Admin, + Password: string(hash), + Salt: uniuri.NewLen(uniuri.UUIDLen), + Created: time.Now().UnixMilli(), + Updated: time.Now().UnixMilli(), + } + + if ok, err := check.User(user); !ok { + render.BadRequest(w, err) + log.Debug().Err(err). + Str("user_email", user.Email). + Msg("cannot validate user") + return + } + + err = users.Create(ctx, user) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot create user") + } else { + render.JSON(w, user, 200) + } + } +} diff --git a/internal/api/handler/users/create_test.go b/internal/api/handler/users/create_test.go new file mode 100644 index 000000000..eb3a125de --- /dev/null +++ b/internal/api/handler/users/create_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users diff --git a/internal/api/handler/users/delete.go b/internal/api/handler/users/delete.go new file mode 100644 index 000000000..dd4958aad --- /dev/null +++ b/internal/api/handler/users/delete.go @@ -0,0 +1,45 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/rs/zerolog/hlog" + + "github.com/go-chi/chi" +) + +// HandleDelete returns an http.HandlerFunc that processes an http.Request +// to delete the named user account from the system. +func HandleDelete(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + key := chi.URLParam(r, "user") + user, err := users.FindKey(ctx, key) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("user_key", key). + Msg("cannot find user") + return + } + err = users.Delete(ctx, user) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot delete user") + + } else { + w.WriteHeader(http.StatusNoContent) + } + } +} diff --git a/internal/api/handler/users/delete_test.go b/internal/api/handler/users/delete_test.go new file mode 100644 index 000000000..eb3a125de --- /dev/null +++ b/internal/api/handler/users/delete_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users diff --git a/internal/api/handler/users/find.go b/internal/api/handler/users/find.go new file mode 100644 index 000000000..3c73914b1 --- /dev/null +++ b/internal/api/handler/users/find.go @@ -0,0 +1,35 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/rs/zerolog/hlog" + + "github.com/go-chi/chi" +) + +// HandleFind returns an http.HandlerFunc that writes json-encoded +// user account information to the the response body. +func HandleFind(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + key := chi.URLParam(r, "user") + user, err := users.FindKey(ctx, key) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("user_key", key). + Msg("cannot find user") + } else { + render.JSON(w, user, 200) + } + } +} diff --git a/internal/api/handler/users/find_test.go b/internal/api/handler/users/find_test.go new file mode 100644 index 000000000..eb3a125de --- /dev/null +++ b/internal/api/handler/users/find_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users diff --git a/internal/api/handler/users/list.go b/internal/api/handler/users/list.go new file mode 100644 index 000000000..987f58898 --- /dev/null +++ b/internal/api/handler/users/list.go @@ -0,0 +1,49 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types/enum" + "github.com/rs/zerolog/hlog" +) + +// HandleList returns an http.HandlerFunc that writes a json-encoded +// list of all registered system users to the response body. +func HandleList(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + log = hlog.FromRequest(r) + ) + + params := request.ParseUserFilter(r) + if params.Order == enum.OrderDefault { + params.Order = enum.OrderAsc + } + + count, err := users.Count(ctx) + if err != nil { + log.Error().Err(err). + Msg("cannot retrieve user count") + } + + list, err := users.List(ctx, params) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Msg("cannot retrieve user list") + return + } + + render.Pagination(r, w, params.Page, params.Size, int(count)) + render.JSON(w, list, 200) + + } +} diff --git a/internal/api/handler/users/list_test.go b/internal/api/handler/users/list_test.go new file mode 100644 index 000000000..eb3a125de --- /dev/null +++ b/internal/api/handler/users/list_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users diff --git a/internal/api/handler/users/update.go b/internal/api/handler/users/update.go new file mode 100644 index 000000000..90c129034 --- /dev/null +++ b/internal/api/handler/users/update.go @@ -0,0 +1,112 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/check" + "github.com/gotidy/ptr" + "github.com/rs/zerolog/hlog" + + "github.com/go-chi/chi" + "golang.org/x/crypto/bcrypt" +) + +// GenerateFromPassword returns the bcrypt hash of the +// password at the given cost. +var hashPassword = bcrypt.GenerateFromPassword + +// HandleUpdate returns an http.HandlerFunc that processes an http.Request +// to update a user account. +func HandleUpdate(users store.UserStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := hlog.FromRequest(r) + + key := chi.URLParam(r, "user") + user, err := users.FindKey(ctx, key) + if err != nil { + render.NotFound(w, err) + log.Debug().Err(err). + Str("user_key", key). + Msg("cannot find user") + return + } + + in := new(types.UserInput) + if err := json.NewDecoder(r.Body).Decode(in); err != nil { + render.BadRequest(w, err) + log.Debug().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot unmarshal request") + return + } + + if in.Password != nil { + hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) + if err != nil { + render.InternalError(w, err) + log.Debug().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot hash password") + return + } + user.Password = string(hash) + } + + if in.Name != nil { + user.Name = ptr.ToString(in.Name) + } + + if in.Company != nil { + user.Company = ptr.ToString(in.Company) + } + + if in.Admin != nil { + user.Admin = ptr.ToBool(in.Admin) + } + + if in.Password != nil { + hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) + if err != nil { + render.InternalError(w, err) + log.Debug().Err(err). + Msg("cannot hash password") + return + } + user.Password = string(hash) + } + + if ok, err := check.User(user); !ok { + render.BadRequest(w, err) + log.Debug().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot update user") + return + } + + user.Updated = time.Now().UnixMilli() + + err = users.Update(ctx, user) + if err != nil { + render.InternalError(w, err) + log.Error().Err(err). + Int64("user_id", user.ID). + Str("user_email", user.Email). + Msg("cannot update user") + } else { + render.JSON(w, user, 200) + } + } +} diff --git a/internal/api/handler/users/update_test.go b/internal/api/handler/users/update_test.go new file mode 100644 index 000000000..eb3a125de --- /dev/null +++ b/internal/api/handler/users/update_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package users diff --git a/internal/api/middleware/access/access.go b/internal/api/middleware/access/access.go new file mode 100644 index 000000000..51f15c1af --- /dev/null +++ b/internal/api/middleware/access/access.go @@ -0,0 +1,33 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package access + +import ( + "errors" + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" +) + +// SystemAdmin returns an http.HandlerFunc middleware that authorizes +// the user access to system administration capabilities. +func SystemAdmin() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + user, ok := request.UserFrom(ctx) + if !ok { + render.ErrorCode(w, errors.New("Requires authentication"), 401) + return + } + if !user.Admin { + render.ErrorCode(w, errors.New("Forbidden"), 403) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/api/middleware/access/access_test.go b/internal/api/middleware/access/access_test.go new file mode 100644 index 000000000..9b173c5ba --- /dev/null +++ b/internal/api/middleware/access/access_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package access diff --git a/internal/api/middleware/address/address.go b/internal/api/middleware/address/address.go new file mode 100644 index 000000000..f133a11f0 --- /dev/null +++ b/internal/api/middleware/address/address.go @@ -0,0 +1,81 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package address + +import ( + "net/http" + "strings" +) + +// Handler returns an http.HandlerFunc middleware that sets +// the http.Request scheme and hostname. +func Handler(scheme, host string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // update the scheme and host for the inbound + // http.Request so they are available to subsequent + // handlers in the chain. + r.URL.Scheme = scheme + r.URL.Host = host + + // if the scheme is not configured, attempt to ascertain + // the scheme from the inbound http.Request. + if r.URL.Scheme == "" { + r.URL.Scheme = resolveScheme(r) + } + + // if the host is not configured, attempt to ascertain + // the host from the inbound http.Request. + if r.URL.Host == "" { + r.URL.Host = resolveHost(r) + } + + // invoke the next handler in the chain. + next.ServeHTTP(w, r) + }) + } +} + +// resolveScheme is a helper function that evaluates the http.Request +// and returns the scheme, HTTP or HTTPS. It is able to detect, +// using the X-Forwarded-Proto, if the original request was HTTPS +// and routed through a reverse proxy with SSL termination. +func resolveScheme(r *http.Request) string { + switch { + case r.URL.Scheme == "https": + return "https" + case r.TLS != nil: + return "https" + case strings.HasPrefix(r.Proto, "HTTPS"): + return "https" + case r.Header.Get("X-Forwarded-Proto") == "https": + return "https" + default: + return "http" + } +} + +// resolveHost is a helper function that evaluates the http.Request +// and returns the hostname. It is able to detect, using the +// X-Forarded-For header, the original hostname when routed +// through a reverse proxy. +func resolveHost(r *http.Request) string { + switch { + case len(r.Host) != 0: + return r.Host + case len(r.URL.Host) != 0: + return r.URL.Host + case len(r.Header.Get("X-Forwarded-For")) != 0: + return r.Header.Get("X-Forwarded-For") + case len(r.Header.Get("X-Host")) != 0: + return r.Header.Get("X-Host") + case len(r.Header.Get("XFF")) != 0: + return r.Header.Get("XFF") + case len(r.Header.Get("X-Real-IP")) != 0: + return r.Header.Get("X-Real-IP") + default: + return "localhost:3000" + } +} diff --git a/internal/api/middleware/address/address_test.go b/internal/api/middleware/address/address_test.go new file mode 100644 index 000000000..c186dc746 --- /dev/null +++ b/internal/api/middleware/address/address_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package address diff --git a/internal/api/middleware/token/token.go b/internal/api/middleware/token/token.go new file mode 100644 index 000000000..c70e4655e --- /dev/null +++ b/internal/api/middleware/token/token.go @@ -0,0 +1,101 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package token + +import ( + "errors" + "net/http" + "strconv" + "strings" + "time" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/internal/api/request" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/token" + "github.com/bradrydzewski/my-app/types" + + "github.com/dgrijalva/jwt-go" + "github.com/rs/zerolog" + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" +) + +// Must returns an http.HandlerFunc middleware that authenticates +// the http.Request and errors if the account cannot be authenticated. +func Must(users store.UserStore) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + str := extractToken(r) + + if len(str) == 0 { + render.ErrorCode(w, errors.New("Requires authentication"), 401) + return + } + + var user *types.User + parsed, err := jwt.ParseWithClaims(str, &token.Claims{}, func(token_ *jwt.Token) (interface{}, error) { + sub := token_.Claims.(*token.Claims).Subject + id, err := strconv.ParseInt(sub, 10, 64) + if err != nil { + return nil, err + } + + user, err = users.Find(ctx, id) + if err != nil { + hlog.FromRequest(r). + Error().Err(err). + Int64("user", id). + Msg("cannot find user") + return nil, err + } + return []byte(user.Salt), nil + }) + if err != nil { + render.ErrorCode(w, err, 401) + return + } + if parsed.Valid == false { + render.ErrorCode(w, errors.New("Invalid token"), 401) + return + } + if _, ok := parsed.Method.(*jwt.SigningMethodHMAC); !ok { + render.ErrorCode(w, errors.New("Invalid token"), 401) + return + } + + // this code should be deprecated, since the jwt.ParseWithClaims + // should fail if the token is expired. TODO remove once we have + // proper unit tests in place. + if claims, ok := parsed.Claims.(*token.Claims); ok { + if claims.ExpiresAt > 0 { + if time.Now().Unix() > claims.ExpiresAt { + render.ErrorCode(w, errors.New("Expired token"), 401) + return + } + } + } + + log.Ctx(ctx).UpdateContext(func(c zerolog.Context) zerolog.Context { + return c.Str("session_email", user.Email).Bool("session_admin", user.Admin) + }) + + next.ServeHTTP(w, r.WithContext( + request.WithUser(ctx, user), + )) + }) + } +} + +func extractToken(r *http.Request) string { + bearer := r.Header.Get("Authorization") + if bearer == "" { + return r.FormValue("access_token") + } + bearer = strings.TrimPrefix(bearer, "Bearer ") + bearer = strings.TrimPrefix(bearer, "IdentityService ") + return bearer +} diff --git a/internal/api/middleware/token/token_test.go b/internal/api/middleware/token/token_test.go new file mode 100644 index 000000000..98b064fa0 --- /dev/null +++ b/internal/api/middleware/token/token_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package token diff --git a/internal/api/openapi/account.go b/internal/api/openapi/account.go new file mode 100644 index 000000000..01bef4549 --- /dev/null +++ b/internal/api/openapi/account.go @@ -0,0 +1,50 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +// request to login to an account. +type loginRequest struct { + Username string `formData:"username"` + Password string `formData:"password"` +} + +// request to register an account. +type registerRequest struct { + Username string `formData:"username"` + Password string `formData:"password"` +} + +// helper function that constructs the openapi specification +// for the account registration and login endpoints. +func buildAccount(reflector *openapi3.Reflector) { + + onLogin := openapi3.Operation{} + onLogin.WithTags("account") + onLogin.WithMapOfAnything(map[string]interface{}{"operationId": "onLogin"}) + reflector.SetRequest(&onLogin, new(loginRequest), http.MethodPost) + reflector.SetJSONResponse(&onLogin, new(types.Token), http.StatusOK) + reflector.SetJSONResponse(&onLogin, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&onLogin, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&onLogin, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPost, "/login", onLogin) + + onRegister := openapi3.Operation{} + onRegister.WithTags("account") + onRegister.WithMapOfAnything(map[string]interface{}{"operationId": "onRegister"}) + reflector.SetRequest(&onRegister, new(registerRequest), http.MethodPost) + reflector.SetJSONResponse(&onRegister, new(types.Token), http.StatusOK) + reflector.SetJSONResponse(&onRegister, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&onRegister, new(render.Error), http.StatusBadRequest) + reflector.Spec.AddOperation(http.MethodPost, "/register", onRegister) +} diff --git a/internal/api/openapi/execution.go b/internal/api/openapi/execution.go new file mode 100644 index 000000000..54d605854 --- /dev/null +++ b/internal/api/openapi/execution.go @@ -0,0 +1,111 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type ( + // request to find or delete a execution. + executionRequest struct { + Pipeline string `path:"pipeline"` + Execution string `path:"execution"` + + // include account parameters + baseRequest + } + + // request to list all executions + executionListRequest struct { + Pipeline string `path:"pipeline"` + + // include pagination + paginationRequest + + // include account parameters + baseRequest + } + + // request to create a execution. + executionCreateRequest struct { + Pipeline string `path:"pipeline"` + + // include request body input. + types.ExecutionInput + + // include account parameters + baseRequest + } + + // request to update a execution. + executionUpdateRequest struct { + Pipeline string `path:"pipeline"` + Execution string `path:"execution"` + + // include request body input. + types.ExecutionInput + + // include account parameters + baseRequest + } +) + +// helper function that constructs the openapi specification +// for execution resources. +func buildExecution(reflector *openapi3.Reflector) { + + opFind := openapi3.Operation{} + opFind.WithTags("execution") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getExecution"}) + reflector.SetRequest(&opFind, new(executionRequest), http.MethodGet) + reflector.SetJSONResponse(&opFind, new(types.Execution), http.StatusOK) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodGet, "/pipelines/{pipeline}/executions/{execution}", opFind) + + opList := openapi3.Operation{} + opList.WithTags("execution") + opList.WithMapOfAnything(map[string]interface{}{"operationId": "listExecutions"}) + reflector.SetRequest(&opList, new(executionListRequest), http.MethodGet) + reflector.SetJSONResponse(&opList, new([]*types.Execution), http.StatusOK) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodGet, "/pipelines/{pipeline}/executions", opList) + + opCreate := openapi3.Operation{} + opCreate.WithTags("execution") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createExecution"}) + reflector.SetRequest(&opCreate, new(executionCreateRequest), http.MethodPost) + reflector.SetJSONResponse(&opCreate, new(types.Execution), http.StatusOK) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPost, "/pipelines/{pipeline}/executions", opCreate) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("execution") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateExecution"}) + reflector.SetRequest(&opUpdate, new(executionUpdateRequest), http.MethodPatch) + reflector.SetJSONResponse(&opUpdate, new(types.Execution), http.StatusOK) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPatch, "/pipelines/{pipeline}/executions/{execution}", opUpdate) + + opDelete := openapi3.Operation{} + opDelete.WithTags("execution") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteExecution"}) + reflector.SetRequest(&opDelete, new(executionRequest), http.MethodDelete) + reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodDelete, "/pipelines/{pipeline}/executions/{execution}", opDelete) +} diff --git a/internal/api/openapi/openapi.go b/internal/api/openapi/openapi.go new file mode 100644 index 000000000..b234fa511 --- /dev/null +++ b/internal/api/openapi/openapi.go @@ -0,0 +1,120 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "bytes" + "net/http" + "strings" + + "github.com/bradrydzewski/my-app/version" + + "github.com/swaggest/openapi-go/openapi3" +) + +type ( + // base request + baseRequest struct { + Account string `query:"accountIdentifier"` + Organization string `query:"orgIdentifier"` + Project string `query:"projectIdentifier"` + Routing string `query:"routingId"` + } + + // base request for pagination + paginationRequest struct { + Page int `query:"page" default:"1"` + Size int `query:"per_page" default:"100"` + } + + // base response for pagination + paginationResponse struct { + Total int `header:"x-total"` + Pagelen int `header:"x-total-pages"` + Page int `header:"x-page"` + Size int `header:"x-per-page"` + Next int `header:"x-next"` + Prev int `header:"x-prev"` + Link []string `header:"Link"` + } +) + +// Handler returns an http.HandlerFunc that writes the openapi v3 +// specification file to the http.Response body. +func Handler() http.HandlerFunc { + spec := Generate() + yaml, _ := spec.MarshalYAML() + json, _ := spec.MarshalJSON() + + yaml = normalize(yaml) + json = normalize(json) + + return func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.URL.Path, ".json"): + w.Write(json) + default: + w.Write(yaml) + } + } +} + +// Generate is a helper function that constructs the +// openapi specification object, which can be marshaled +// to json or yaml, as needed. +func Generate() *openapi3.Spec { + reflector := openapi3.Reflector{} + reflector.Spec = &openapi3.Spec{Openapi: "3.0.0"} + reflector.Spec.Info. + WithTitle("API Specification"). + WithVersion(version.Version.String()) + + // + // register endpoints + // + + buildAccount(&reflector) + buildPipeline(&reflector) + buildExecution(&reflector) + buildUser(&reflector) + buildUsers(&reflector) + buildProjects(&reflector) + + // + // define security scheme + // + + scheme := openapi3.SecuritySchemeOrRef{ + SecurityScheme: &openapi3.SecurityScheme{ + HTTPSecurityScheme: &openapi3.HTTPSecurityScheme{ + Scheme: "bearerAuth", + Bearer: &openapi3.Bearer{}, + }, + }, + } + security := openapi3.ComponentsSecuritySchemes{} + security.WithMapOfSecuritySchemeOrRefValuesItem("bearerAuth", scheme) + reflector.Spec.Components.WithSecuritySchemes(security) + + // + // enforce security scheme globally + // + + reflector.Spec.WithSecurity(map[string][]string{ + "bearerAuth": {}, + }) + + return reflector.Spec +} + +// helper function normalizes the output to ensure +// automatically-generated names are more user friendly. +func normalize(data []byte) []byte { + data = bytes.ReplaceAll(data, []byte("Types"), []byte("")) + data = bytes.ReplaceAll(data, []byte("Openapi"), []byte("")) + data = bytes.ReplaceAll(data, []byte("FormData"), []byte("")) + data = bytes.ReplaceAll(data, []byte("RenderError"), []byte("Error")) + return data +} diff --git a/internal/api/openapi/openapi_test.go b/internal/api/openapi/openapi_test.go new file mode 100644 index 000000000..bf1f643dc --- /dev/null +++ b/internal/api/openapi/openapi_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi diff --git a/internal/api/openapi/pipeline.go b/internal/api/openapi/pipeline.go new file mode 100644 index 000000000..c352588ff --- /dev/null +++ b/internal/api/openapi/pipeline.go @@ -0,0 +1,103 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type ( + // request to find or delete a pipeline. + pipelineRequest struct { + Param string `path:"pipeline"` + + // include account parameters + baseRequest + } + + pipelineListRequest struct { + // include account parameters + baseRequest + + // include pagination parameters + paginationRequest + } + + // request to update a pipeline. + pipelineUpdateRequest struct { + Param string `path:"pipeline"` + + // include request body input. + types.PipelineInput + + // include account parameters + baseRequest + } + + // request to create a pipeline. + pipelineCreateRequest struct { + // include account parameters + baseRequest + + // include request body input. + types.PipelineInput + } +) + +// helper function that constructs the openapi specification +// for pipeline resources. +func buildPipeline(reflector *openapi3.Reflector) { + + opFind := openapi3.Operation{} + opFind.WithTags("pipeline") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getPipeline"}) + reflector.SetRequest(&opFind, new(pipelineRequest), http.MethodGet) + reflector.SetJSONResponse(&opFind, new(types.Pipeline), http.StatusOK) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodGet, "/pipelines/{pipeline}", opFind) + + onList := openapi3.Operation{} + onList.WithTags("pipeline") + onList.WithMapOfAnything(map[string]interface{}{"operationId": "listPipelines"}) + reflector.SetRequest(&onList, new(pipelineListRequest), http.MethodGet) + reflector.SetJSONResponse(&onList, new([]*types.Pipeline), http.StatusOK) + reflector.SetJSONResponse(&onList, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodGet, "/pipelines", onList) + + opCreate := openapi3.Operation{} + opCreate.WithTags("pipeline") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createPipeline"}) + reflector.SetRequest(&opCreate, new(pipelineCreateRequest), http.MethodPost) + reflector.SetJSONResponse(&opCreate, new(types.Pipeline), http.StatusOK) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPost, "/pipelines", opCreate) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("pipeline") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updatePipeline"}) + reflector.SetRequest(&opUpdate, new(pipelineUpdateRequest), http.MethodPatch) + reflector.SetJSONResponse(&opUpdate, new(types.Pipeline), http.StatusOK) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPatch, "/pipelines/{pipeline}", opUpdate) + + opDelete := openapi3.Operation{} + opDelete.WithTags("pipeline") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deletePipeline"}) + reflector.SetRequest(&opDelete, new(pipelineRequest), http.MethodDelete) + reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodDelete, "/pipelines/{pipeline}", opDelete) +} diff --git a/internal/api/openapi/projects.go b/internal/api/openapi/projects.go new file mode 100644 index 000000000..469cb3aa8 --- /dev/null +++ b/internal/api/openapi/projects.go @@ -0,0 +1,63 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type ( + projectRequest struct { + Project string `path:"project"` + + // include base request + baseRequest + } + + projectListRequest struct { + // include pagination request + paginationRequest + + // include base request + baseRequest + } + + projectResponse struct { + Data types.Project `json:"data"` + Status string `json:"status" enum:"SUCCESS,FAILURE,ERROR"` + } + + projectListResponse struct { + Data types.ProjectList `json:"data"` + Status string `json:"status" enum:"SUCCESS,FAILURE,ERROR"` + } +) + +// helper function that constructs the openapi specification +// for project resources. +func buildProjects(reflector *openapi3.Reflector) { + + opFind := openapi3.Operation{} + opFind.WithTags("projects") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getProject"}) + reflector.SetRequest(&opFind, new(projectRequest), http.MethodGet) + reflector.SetJSONResponse(&opFind, new(projectResponse), http.StatusOK) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodGet, "/api/projects/{project}", opFind) + + opList := openapi3.Operation{} + opList.WithTags("projects", "user") + opList.WithMapOfAnything(map[string]interface{}{"operationId": "listProjects"}) + reflector.SetRequest(&opList, new(projectListRequest), http.MethodGet) + reflector.SetJSONResponse(&opList, new(projectListResponse), http.StatusOK) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodGet, "/api/user/projects", opList) + +} diff --git a/internal/api/openapi/user.go b/internal/api/openapi/user.go new file mode 100644 index 000000000..83dfc8d2c --- /dev/null +++ b/internal/api/openapi/user.go @@ -0,0 +1,56 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type currentUserResponse struct { + Data *types.User `json:"data"` + Status string `json:"status" enum:"SUCCESS,FAILURE,ERROR"` +} + +// helper function that constructs the openapi specification +// for user account resources. +func buildUser(reflector *openapi3.Reflector) { + + opFind := openapi3.Operation{} + opFind.WithTags("user") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getUser"}) + reflector.SetRequest(&opFind, nil, http.MethodGet) + reflector.SetJSONResponse(&opFind, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodGet, "/user", opFind) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("user") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateUser"}) + reflector.SetRequest(&opUpdate, new(types.UserInput), http.MethodPatch) + reflector.SetJSONResponse(&opUpdate, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodPatch, "/user", opUpdate) + + opToken := openapi3.Operation{} + opToken.WithTags("user") + opToken.WithMapOfAnything(map[string]interface{}{"operationId": "createToken"}) + reflector.SetRequest(&opToken, new(types.Token), http.MethodPost) + reflector.SetJSONResponse(&opToken, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opToken, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodPost, "/user/token", opToken) + + opCurrent := openapi3.Operation{} + opCurrent.WithTags("user") + opCurrent.WithMapOfAnything(map[string]interface{}{"operationId": "getCurrentUser"}) + reflector.SetRequest(&opFind, new(baseRequest), http.MethodGet) + reflector.SetJSONResponse(&opCurrent, new(currentUserResponse), http.StatusOK) + reflector.SetJSONResponse(&opCurrent, new(render.Error), http.StatusInternalServerError) + reflector.Spec.AddOperation(http.MethodGet, "/api/user/currentUser", opCurrent) +} diff --git a/internal/api/openapi/users.go b/internal/api/openapi/users.go new file mode 100644 index 000000000..dec6abdd3 --- /dev/null +++ b/internal/api/openapi/users.go @@ -0,0 +1,92 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" + "github.com/bradrydzewski/my-app/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type ( + // request for finding or deleting a user. + userRequest struct { + Param string `path:"email"` + } + + // request for listing users. + userListRequest struct { + Sort string `query:"sort" enum:"id,email,created,updated"` + Order string `query:"direction" enum:"asc,desc"` + + // include pagination request + paginationRequest + } + + // request for updating a user. + userUpdateRequest struct { + Param string `path:"email"` + + // include request body input. + types.UserInput + } +) + +// helper function that constructs the openapi specification +// for user resources. +func buildUsers(reflector *openapi3.Reflector) { + + opFind := openapi3.Operation{} + opFind.WithTags("users") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getUserEmail"}) + reflector.SetRequest(&opFind, new(userRequest), http.MethodGet) + reflector.SetJSONResponse(&opFind, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opFind, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodGet, "/users/{email}", opFind) + + opList := openapi3.Operation{} + opList.WithTags("users") + opList.WithMapOfAnything(map[string]interface{}{"operationId": "listUsers"}) + reflector.SetRequest(&opList, new(userListRequest), http.MethodGet) + reflector.SetJSONResponse(&opList, new([]*types.User), http.StatusOK) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opList, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodGet, "/users", opList) + + opCreate := openapi3.Operation{} + opCreate.WithTags("users") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createUser"}) + reflector.SetRequest(&opCreate, new(types.UserInput), http.MethodPost) + reflector.SetJSONResponse(&opCreate, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opCreate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPost, "/users", opCreate) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("users") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateUsers"}) + reflector.SetRequest(&opUpdate, new(userUpdateRequest), http.MethodPatch) + reflector.SetJSONResponse(&opUpdate, new(types.User), http.StatusOK) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusBadRequest) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opUpdate, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodPatch, "/users/{email}", opUpdate) + + opDelete := openapi3.Operation{} + opDelete.WithTags("users") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteUser"}) + reflector.SetRequest(&opDelete, new(userRequest), http.MethodDelete) + reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusInternalServerError) + reflector.SetJSONResponse(&opDelete, new(render.Error), http.StatusNotFound) + reflector.Spec.AddOperation(http.MethodDelete, "/users/{email}", opDelete) +} diff --git a/internal/api/render/errors.go b/internal/api/render/errors.go new file mode 100644 index 000000000..fb57abfb6 --- /dev/null +++ b/internal/api/render/errors.go @@ -0,0 +1,33 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +var ( + // ErrInvalidToken is returned when the api request token is invalid. + ErrInvalidToken = New("Invalid or missing token") + + // ErrUnauthorized is returned when the user is not authorized. + ErrUnauthorized = New("Unauthorized") + + // ErrForbidden is returned when user access is forbidden. + ErrForbidden = New("Forbidden") + + // ErrNotFound is returned when a resource is not found. + ErrNotFound = New("Not Found") +) + +// Error represents a json-encoded API error. +type Error struct { + Message string `json:"message"` +} + +func (e *Error) Error() string { + return e.Message +} + +// New returns a new error message. +func New(text string) error { + return &Error{Message: text} +} diff --git a/internal/api/render/errors_test.go b/internal/api/render/errors_test.go new file mode 100644 index 000000000..e93939c6e --- /dev/null +++ b/internal/api/render/errors_test.go @@ -0,0 +1,14 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +import "testing" + +func TestError(t *testing.T) { + got, want := ErrNotFound.Error(), ErrNotFound.(*Error).Message + if got != want { + t.Errorf("Want error string %q, got %q", got, want) + } +} diff --git a/internal/api/render/header.go b/internal/api/render/header.go new file mode 100644 index 000000000..bb9d46757 --- /dev/null +++ b/internal/api/render/header.go @@ -0,0 +1,68 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +import ( + "fmt" + "net/http" + "strconv" +) + +// format string for the link header value. +var linkf = `<%s>; rel="%s"` + +// Pagination writes the pagination and link headers +// to the http.Response. +func Pagination(r *http.Request, w http.ResponseWriter, page, size, total int) { + var ( + last = pagelen(size, total) + next = min(page+1, last) + prev = max(page-1, 1) + ) + + uri := *r.URL + + // parse the existing query parameters and + // sanize parameter list. + params := uri.Query() + params.Del("access_token") + params.Del("token") + params.Set("page", strconv.Itoa(page)) + params.Set("per_page", strconv.Itoa(size)) + + w.Header().Set("x-page", strconv.Itoa(page)) + w.Header().Set("x-per-page", strconv.Itoa(size)) + + if page != last { + // update the page query parameter and re-encode + params.Set("page", strconv.Itoa(next)) + uri.RawQuery = params.Encode() + + // write the next page to the header. + w.Header().Set("x-next-page", strconv.Itoa(next)) + w.Header().Add("Link", fmt.Sprintf(linkf, uri.String(), "next")) + } + + if page > 1 { + // update the page query parameter and re-encode. + params.Set("page", strconv.Itoa(prev)) + uri.RawQuery = params.Encode() + + // write the previous page to the header. + w.Header().Set("x-prev-page", strconv.Itoa(prev)) + w.Header().Add("Link", fmt.Sprintf(linkf, uri.String(), "prev")) + } + + { + // update the page query parameter and re-encode + params.Set("page", strconv.Itoa(last)) + uri.RawQuery = params.Encode() + + // write the page total to the header. + w.Header().Set("x-total", strconv.Itoa(total)) + w.Header().Set("x-total-pages", strconv.Itoa(last)) + w.Header().Add("Link", fmt.Sprintf(linkf, uri.String(), "last")) + } +} diff --git a/internal/api/render/header_test.go b/internal/api/render/header_test.go new file mode 100644 index 000000000..3efccd684 --- /dev/null +++ b/internal/api/render/header_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render diff --git a/internal/api/render/platform/render.go b/internal/api/render/platform/render.go new file mode 100644 index 000000000..12be34fa1 --- /dev/null +++ b/internal/api/render/platform/render.go @@ -0,0 +1,28 @@ +package platform + +import ( + "encoding/json" + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/render" +) + +// RenderResource is a helper function that renders a single +// resource, wrapped in the harness payload envelope. +func RenderResource(w http.ResponseWriter, v interface{}, code int) { + payload := new(wrapper) + payload.Status = "SUCCESS" + payload.Data, _ = json.Marshal(v) + if code > 399 { + payload.Status = "ERROR" + } else if code > 299 { + payload.Status = "FAILURE" + } + render.JSON(w, payload, code) +} + +// wrapper defines the payload wrapper. +type wrapper struct { + Status string `json:"status"` + Data json.RawMessage `json:"data"` +} diff --git a/internal/api/render/render.go b/internal/api/render/render.go new file mode 100644 index 000000000..240b4408b --- /dev/null +++ b/internal/api/render/render.go @@ -0,0 +1,88 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" +) + +// indent the json-encoded API responses +var indent bool + +func init() { + indent, _ = strconv.ParseBool( + os.Getenv("HTTP_JSON_INDENT"), + ) +} + +// ErrorCode writes the json-encoded error message to the response. +func ErrorCode(w http.ResponseWriter, err error, status int) { + JSON(w, &Error{Message: err.Error()}, status) +} + +// InternalError writes the json-encoded error message to the response +// with a 500 internal server error. +func InternalError(w http.ResponseWriter, err error) { + ErrorCode(w, err, 500) +} + +// InternalErrorf writes the json-encoded error message to the response +// with a 500 internal server error. +func InternalErrorf(w http.ResponseWriter, format string, a ...interface{}) { + ErrorCode(w, fmt.Errorf(format, a...), 500) +} + +// NotFound writes the json-encoded error message to the response +// with a 404 not found status code. +func NotFound(w http.ResponseWriter, err error) { + ErrorCode(w, err, 404) +} + +// NotFoundf writes the json-encoded error message to the response +// with a 404 not found status code. +func NotFoundf(w http.ResponseWriter, format string, a ...interface{}) { + ErrorCode(w, fmt.Errorf(format, a...), 404) +} + +// Unauthorized writes the json-encoded error message to the response +// with a 401 unauthorized status code. +func Unauthorized(w http.ResponseWriter, err error) { + ErrorCode(w, err, 401) +} + +// Forbidden writes the json-encoded error message to the response +// with a 403 forbidden status code. +func Forbidden(w http.ResponseWriter, err error) { + ErrorCode(w, err, 403) +} + +// BadRequest writes the json-encoded error message to the response +// with a 400 bad request status code. +func BadRequest(w http.ResponseWriter, err error) { + ErrorCode(w, err, 400) +} + +// BadRequestf writes the json-encoded error message to the response +// with a 400 bad request status code. +func BadRequestf(w http.ResponseWriter, format string, a ...interface{}) { + ErrorCode(w, fmt.Errorf(format, a...), 400) +} + +// JSON writes the json-encoded error message to the response +// with a 400 bad request status code. +func JSON(w http.ResponseWriter, v interface{}, status int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(status) + enc := json.NewEncoder(w) + if indent { + enc.SetIndent("", " ") + } + enc.Encode(v) +} diff --git a/internal/api/render/render_test.go b/internal/api/render/render_test.go new file mode 100644 index 000000000..918dc94bd --- /dev/null +++ b/internal/api/render/render_test.go @@ -0,0 +1,191 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestWriteError(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + InternalError(w, err) + + if got, want := w.Code, 500; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteErrorf(t *testing.T) { + w := httptest.NewRecorder() + + InternalErrorf(w, "pc load letter") + + if got, want := w.Code, 500; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, "pc load letter"; got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteErrorCode(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + ErrorCode(w, err, 418) + + if got, want := w.Code, 418; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteNotFound(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + NotFound(w, err) + + if got, want := w.Code, 404; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteNotFoundf(t *testing.T) { + w := httptest.NewRecorder() + + NotFoundf(w, "pc load letter") + + if got, want := w.Code, 404; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, "pc load letter"; got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteUnauthorized(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + Unauthorized(w, err) + + if got, want := w.Code, 401; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteForbidden(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + Forbidden(w, err) + + if got, want := w.Code, 403; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteBadRequest(t *testing.T) { + w := httptest.NewRecorder() + + err := New("pc load letter") + BadRequest(w, err) + + if got, want := w.Code, 400; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, err.Error(); got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteBadRequestf(t *testing.T) { + w := httptest.NewRecorder() + + BadRequestf(w, "pc load letter") + + if got, want := w.Code, 400; want != got { + t.Errorf("Want response code %d, got %d", want, got) + } + + errjson := &Error{} + json.NewDecoder(w.Body).Decode(errjson) + if got, want := errjson.Message, "pc load letter"; got != want { + t.Errorf("Want error message %s, got %s", want, got) + } +} + +func TestWriteJSON(t *testing.T) { + // without indent + { + w := httptest.NewRecorder() + JSON(w, map[string]string{"hello": "world"}, http.StatusTeapot) + if got, want := w.Body.String(), "{\"hello\":\"world\"}\n"; got != want { + t.Errorf("Want JSON body %q, got %q", want, got) + } + if got, want := w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8"; got != want { + t.Errorf("Want Content-Type %q, got %q", want, got) + } + if got, want := w.Code, http.StatusTeapot; got != want { + t.Errorf("Want status code %d, got %d", want, got) + } + } + // with indent + { + indent = true + defer func() { + indent = false + }() + w := httptest.NewRecorder() + JSON(w, map[string]string{"hello": "world"}, http.StatusTeapot) + if got, want := w.Body.String(), "{\n \"hello\": \"world\"\n}\n"; got != want { + t.Errorf("Want JSON body %q, got %q", want, got) + } + } +} diff --git a/internal/api/render/util.go b/internal/api/render/util.go new file mode 100644 index 000000000..61541f0da --- /dev/null +++ b/internal/api/render/util.go @@ -0,0 +1,39 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +// pagelen calculates to total number of pages given the +// page size and total count of all paginated items. +func pagelen(size, total int) int { + quotient, remainder := total/size, total%size + switch { + case quotient == 0: + return 1 + case remainder == 0: + return quotient + default: + return quotient + 1 + } +} + +// max returns the larger of x or y. +func max(x, y int) int { + if x > y { + return x + } else { + return y + } +} + +// max returns the smaller of x or y. +func min(x, y int) int { + if y == 0 { + return x + } else if x < y { + return x + } else { + return y + } +} diff --git a/internal/api/render/util_test.go b/internal/api/render/util_test.go new file mode 100644 index 000000000..5d9bd9245 --- /dev/null +++ b/internal/api/render/util_test.go @@ -0,0 +1,28 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package render + +import "testing" + +func Test_pagelen(t *testing.T) { + tests := []struct { + size, total, want int + }{ + {25, 1, 1}, + {25, 24, 1}, + {25, 25, 1}, + {25, 26, 2}, + {25, 49, 2}, + {25, 50, 2}, + {25, 51, 3}, + } + + for _, test := range tests { + got, want := pagelen(test.size, test.total), test.want + if got != want { + t.Errorf("got page length %d, want %d", got, want) + } + } +} diff --git a/internal/api/request/context.go b/internal/api/request/context.go new file mode 100644 index 000000000..fcf4e36d8 --- /dev/null +++ b/internal/api/request/context.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +// This pattern was inpired by the kubernetes request context package. +// https://github.com/kubernetes/apiserver/blob/master/pkg/endpoints/request/context.go + +import ( + "context" + + "github.com/bradrydzewski/my-app/types" +) + +type key int + +const ( + userKey key = iota + projKey +) + +// WithUser returns a copy of parent in which the user +// value is set +func WithUser(parent context.Context, v *types.User) context.Context { + return context.WithValue(parent, userKey, v) +} + +// UserFrom returns the value of the user key on the +// context. +func UserFrom(ctx context.Context) (*types.User, bool) { + v, ok := ctx.Value(userKey).(*types.User) + return v, ok +} diff --git a/internal/api/request/context_test.go b/internal/api/request/context_test.go new file mode 100644 index 000000000..1c45304a9 --- /dev/null +++ b/internal/api/request/context_test.go @@ -0,0 +1,11 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +import "testing" + +func TestContext(t *testing.T) { + t.Skip() +} diff --git a/internal/api/request/util.go b/internal/api/request/util.go new file mode 100644 index 000000000..6de789d39 --- /dev/null +++ b/internal/api/request/util.go @@ -0,0 +1,74 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +import ( + "net/http" + "strconv" + + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/enum" +) + +// ParsePage extracts the page parameter from the url. +func ParsePage(r *http.Request) int { + s := r.FormValue("page") + i, _ := strconv.Atoi(s) + if i == 0 { + i = 1 + } + return i +} + +// ParseSize extracts the size parameter from the url. +func ParseSize(r *http.Request) int { + s := r.FormValue("per_page") + i, _ := strconv.Atoi(s) + if i == 0 { + i = 100 + } else if i > 100 { + i = 100 + } + return i +} + +// ParseOrder extracts the order parameter from the url. +func ParseOrder(r *http.Request) enum.Order { + return enum.ParseOrder( + r.FormValue("direction"), + ) +} + +// ParseSort extracts the sort parameter from the url. +func ParseSort(r *http.Request) (s string) { + return r.FormValue("sort") +} + +// ParseSortUser extracts the user stor parameter from the url. +func ParseSortUser(r *http.Request) enum.UserAttr { + return enum.ParseUserAttr( + r.FormValue("sort"), + ) +} + +// ParseParams extracts the query parameter from the url. +func ParseParams(r *http.Request) types.Params { + return types.Params{ + Order: ParseOrder(r), + Page: ParsePage(r), + Sort: ParseSort(r), + Size: ParseSize(r), + } +} + +// ParseUserFilter extracts the user query parameter from the url. +func ParseUserFilter(r *http.Request) types.UserFilter { + return types.UserFilter{ + Order: ParseOrder(r), + Page: ParsePage(r), + Sort: ParseSortUser(r), + Size: ParseSize(r), + } +} diff --git a/internal/api/request/util_test.go b/internal/api/request/util_test.go new file mode 100644 index 000000000..2b16f486f --- /dev/null +++ b/internal/api/request/util_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request diff --git a/internal/cron/nightly.go b/internal/cron/nightly.go new file mode 100644 index 000000000..bcb419c49 --- /dev/null +++ b/internal/cron/nightly.go @@ -0,0 +1,41 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cron + +import ( + "context" + "time" + + "github.com/rs/zerolog/log" +) + +// helper function returns the current time. +var now = time.Now + +// Nightly is a sub-routine that periodically purges historical data. +type Nightly struct { + // Inject required stores here +} + +// NewNightly returns a new Nightly sub-routine. +func NewNightly() *Nightly { + return &Nightly{} +} + +// Run runs the purge sub-routine. +func (n *Nightly) Run(ctx context.Context) { + ticker := time.NewTicker(time.Hour * 24) + logger := log.Ctx(ctx) + for { + select { + case <-ctx.Done(): + return // break + case <-ticker.C: + // TODO replace this with your nightly + // cron tasks. + logger.Trace().Msg("cron job executed") + } + } +} diff --git a/internal/cron/nightly_test.go b/internal/cron/nightly_test.go new file mode 100644 index 000000000..1d17b7e4a --- /dev/null +++ b/internal/cron/nightly_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cron diff --git a/internal/cron/wire.go b/internal/cron/wire.go new file mode 100644 index 000000000..4d4ed0999 --- /dev/null +++ b/internal/cron/wire.go @@ -0,0 +1,10 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package cron + +import "github.com/google/wire" + +// WireSet provides a wire set for this package +var WireSet = wire.NewSet(NewNightly) diff --git a/internal/inernal_test.go b/internal/inernal_test.go new file mode 100644 index 000000000..1c0522efc --- /dev/null +++ b/internal/inernal_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package internal diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 000000000..1c0522efc --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package internal diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 000000000..425266302 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,204 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package router provides http handlers for serving the +// web applicationa and API endpoints. +package router + +import ( + "context" + "net/http" + + "github.com/bradrydzewski/my-app/internal/api/handler/account" + "github.com/bradrydzewski/my-app/internal/api/handler/executions" + "github.com/bradrydzewski/my-app/internal/api/handler/pipelines" + "github.com/bradrydzewski/my-app/internal/api/handler/projects" + "github.com/bradrydzewski/my-app/internal/api/handler/system" + "github.com/bradrydzewski/my-app/internal/api/handler/user" + "github.com/bradrydzewski/my-app/internal/api/handler/users" + "github.com/bradrydzewski/my-app/internal/api/middleware/access" + "github.com/bradrydzewski/my-app/internal/api/middleware/address" + "github.com/bradrydzewski/my-app/internal/api/middleware/token" + "github.com/bradrydzewski/my-app/internal/api/openapi" + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/web" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" + "github.com/swaggest/swgui/v3emb" + "github.com/unrolled/secure" +) + +// empty context +var nocontext = context.Background() + +// New returns a new http.Handler that routes traffic +// to the appropriate http.Handlers. +func New( + executionStore store.ExecutionStore, + pipelineStore store.PipelineStore, + userStore store.UserStore, + systemStore store.SystemStore, +) http.Handler { + + // create the router with caching disabled + // for API endpoints + r := chi.NewRouter() + + // create the auth middleware. + auth := token.Must(userStore) + + // retrieve system configuration in order to + // retrieve security and cors configuration options. + config := systemStore.Config(nocontext) + + r.Route("/api", func(r chi.Router) { + r.Use(middleware.NoCache) + r.Use(middleware.Recoverer) + + // configure middleware to help ascertain the true + // server address from the incoming http.Request + r.Use( + address.Handler( + config.Server.Proto, + config.Server.Host, + ), + ) + + // configure logging middleware. + r.Use(hlog.NewHandler(log.Logger)) + r.Use(hlog.URLHandler("path")) + r.Use(hlog.MethodHandler("method")) + r.Use(hlog.RequestIDHandler("request", "Request-Id")) + + // configure cors middleware + cors := cors.New( + cors.Options{ + AllowedOrigins: config.Cors.AllowedOrigins, + AllowedMethods: config.Cors.AllowedMethods, + AllowedHeaders: config.Cors.AllowedHeaders, + ExposedHeaders: config.Cors.ExposedHeaders, + AllowCredentials: config.Cors.AllowCredentials, + MaxAge: config.Cors.MaxAge, + }, + ) + r.Use(cors.Handler) + + r.Route("/v1", func(r chi.Router) { + // pipeline endpoints + r.Route("/pipelines", func(r chi.Router) { + r.Use(auth) + r.Get("/", pipelines.HandleList(pipelineStore)) + r.Post("/", pipelines.HandleCreate(pipelineStore)) + + // pipeline endpoints + r.Route("/{pipeline}", func(r chi.Router) { + r.Get("/", pipelines.HandleFind(pipelineStore)) + r.Patch("/", pipelines.HandleUpdate(pipelineStore)) + r.Delete("/", pipelines.HandleDelete(pipelineStore)) + + // execution endpoints + r.Route("/executions", func(r chi.Router) { + r.Get("/", executions.HandleList(pipelineStore, executionStore)) + r.Post("/", executions.HandleCreate(pipelineStore, executionStore)) + r.Get("/{execution}", executions.HandleFind(pipelineStore, executionStore)) + r.Patch("/{execution}", executions.HandleUpdate(pipelineStore, executionStore)) + r.Delete("/{execution}", executions.HandleDelete(pipelineStore, executionStore)) + }) + }) + }) + + // authenticated user endpoints + r.Route("/user", func(r chi.Router) { + r.Use(auth) + + r.Get("/", user.HandleFind()) + r.Patch("/", user.HandleUpdate(userStore)) + r.Post("/token", user.HandleToken(userStore)) + }) + + // user management endpoints + r.Route("/users", func(r chi.Router) { + r.Use(auth) + r.Use(access.SystemAdmin()) + + r.Get("/", users.HandleList(userStore)) + r.Post("/", users.HandleCreate(userStore)) + r.Get("/{user}", users.HandleFind(userStore)) + r.Patch("/{user}", users.HandleUpdate(userStore)) + r.Delete("/{user}", users.HandleDelete(userStore)) + }) + + // system management endpoints + r.Route("/system", func(r chi.Router) { + r.Get("/health", system.HandleHealth) + r.Get("/version", system.HandleVersion) + }) + + // user login endpoint + r.Post("/login", account.HandleLogin(userStore, systemStore)) + + // user registration endpoint + r.Post("/register", account.HandleRegister(userStore, systemStore)) + + // openapi specification endpoints + swagger := openapi.Handler() + r.Handle("/swagger.json", swagger) + r.Handle("/swagger.yaml", swagger) + + }) + + // harness platform project endpoints + r.Route("/projects", func(r chi.Router) { + r.Use(auth) + r.Get("/{project}", projects.HandleFind()) + }) + + // harness platform project endpoints + r.Route("/user", func(r chi.Router) { + r.Use(auth) + r.Get("/currentUser", user.HandleCurrent()) + r.Get("/projects", projects.HandleList()) + }) + }) + + // create middleware to enforce security best practices for + // the user interface. note that theis middleware is only used + // when serving the user interface (not found handler, below). + sec := secure.New( + secure.Options{ + AllowedHosts: config.Secure.AllowedHosts, + HostsProxyHeaders: config.Secure.HostsProxyHeaders, + SSLRedirect: config.Secure.SSLRedirect, + SSLTemporaryRedirect: config.Secure.SSLTemporaryRedirect, + SSLHost: config.Secure.SSLHost, + SSLProxyHeaders: config.Secure.SSLProxyHeaders, + STSSeconds: config.Secure.STSSeconds, + STSIncludeSubdomains: config.Secure.STSIncludeSubdomains, + STSPreload: config.Secure.STSPreload, + ForceSTSHeader: config.Secure.ForceSTSHeader, + FrameDeny: config.Secure.FrameDeny, + ContentTypeNosniff: config.Secure.ContentTypeNosniff, + BrowserXssFilter: config.Secure.BrowserXSSFilter, + ContentSecurityPolicy: config.Secure.ContentSecurityPolicy, + ReferrerPolicy: config.Secure.ReferrerPolicy, + }, + ) + + // openapi playground endpoints + swagger := v3emb.NewHandler("API Definition", "/api/v1/swagger.yaml", "/swagger") + r.With(sec.Handler).Handle("/swagger", swagger) + r.With(sec.Handler).Handle("/swagger/*", swagger) + + // serve all other routes from the embedded filesystem, + // which in turn serves the user interface. + r.With(sec.Handler).NotFound( + web.Handler(), + ) + + return r +} diff --git a/internal/router/router_test.go b/internal/router/router_test.go new file mode 100644 index 000000000..9195215cd --- /dev/null +++ b/internal/router/router_test.go @@ -0,0 +1,28 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package router + +import "testing" + +// this unit test ensures routes that require authorization +// return a 401 unauthorized if no token, or an invalid token +// is provided. +func TestTokenGate(t *testing.T) { + t.Skip() +} + +// this unit test ensures routes that require pipeline access +// return a 403 forbidden if the user does not have acess +// to the pipeline +func TestPipelineGate(t *testing.T) { + t.Skip() +} + +// this unit test ensures routes that require system access +// return a 403 forbidden if the user does not have acess +// to the pipeline +func TestSystemGate(t *testing.T) { + t.Skip() +} diff --git a/internal/router/wire.go b/internal/router/wire.go new file mode 100644 index 000000000..b7d7ce305 --- /dev/null +++ b/internal/router/wire.go @@ -0,0 +1,12 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package router + +import ( + "github.com/google/wire" +) + +// WireSet provides a wire set for this package +var WireSet = wire.NewSet(New) diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 000000000..357e304df --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,124 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package server implements an http server. +package server + +import ( + "context" + "crypto/tls" + "net/http" + + "golang.org/x/crypto/acme/autocert" + "golang.org/x/sync/errgroup" +) + +// A Server defines parameters for running an HTTP server. +type Server struct { + Acme bool + Addr string + Cert string + Key string + Host string + Handler http.Handler +} + +// ListenAndServe initializes a server to respond to HTTP network requests. +func (s *Server) ListenAndServe(ctx context.Context) error { + if s.Acme { + return s.listenAndServeAcme(ctx) + } else if s.Key != "" { + return s.listenAndServeTLS(ctx) + } + return s.listenAndServe(ctx) +} + +func (s *Server) listenAndServe(ctx context.Context) error { + var g errgroup.Group + s1 := &http.Server{ + Addr: s.Addr, + Handler: s.Handler, + } + g.Go(func() error { + select { + case <-ctx.Done(): + return s1.Shutdown(ctx) + } + }) + g.Go(func() error { + return s1.ListenAndServe() + }) + return g.Wait() +} + +func (s *Server) listenAndServeTLS(ctx context.Context) error { + var g errgroup.Group + s1 := &http.Server{ + Addr: ":http", + Handler: http.HandlerFunc(redirect), + } + s2 := &http.Server{ + Addr: ":https", + Handler: s.Handler, + } + g.Go(func() error { + return s1.ListenAndServe() + }) + g.Go(func() error { + return s2.ListenAndServeTLS( + s.Cert, + s.Key, + ) + }) + g.Go(func() error { + select { + case <-ctx.Done(): + s1.Shutdown(ctx) + s2.Shutdown(ctx) + return nil + } + }) + return g.Wait() +} + +func (s Server) listenAndServeAcme(ctx context.Context) error { + var g errgroup.Group + m := &autocert.Manager{ + Cache: autocert.DirCache(".cache"), + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(s.Host), + } + s1 := &http.Server{ + Addr: ":http", + Handler: m.HTTPHandler(nil), + } + s2 := &http.Server{ + Addr: ":https", + Handler: s.Handler, + TLSConfig: &tls.Config{ + GetCertificate: m.GetCertificate, + NextProtos: []string{"h2", "http/1.1"}, + }, + } + g.Go(func() error { + return s1.ListenAndServe() + }) + g.Go(func() error { + return s2.ListenAndServeTLS("", "") + }) + g.Go(func() error { + select { + case <-ctx.Done(): + s1.Shutdown(ctx) + s2.Shutdown(ctx) + return nil + } + }) + return g.Wait() +} + +func redirect(w http.ResponseWriter, req *http.Request) { + target := "https://" + req.Host + req.URL.Path + http.Redirect(w, req, target, http.StatusTemporaryRedirect) +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 000000000..9cc33d7b1 --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package server diff --git a/internal/server/wire.go b/internal/server/wire.go new file mode 100644 index 000000000..a028343dc --- /dev/null +++ b/internal/server/wire.go @@ -0,0 +1,26 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package server + +import ( + "net/http" + + "github.com/bradrydzewski/my-app/types" + + "github.com/google/wire" +) + +// WireSet provides a wire set for this package +var WireSet = wire.NewSet(ProvideServer) + +// ProvideServer provides a server instance +func ProvideServer(config *types.Config, handler http.Handler) *Server { + return &Server{ + Acme: config.Server.Acme.Enabled, + Addr: config.Server.Bind, + Host: config.Server.Host, + Handler: handler, + } +} diff --git a/internal/store/database/execution.go b/internal/store/database/execution.go new file mode 100644 index 000000000..1ec448de5 --- /dev/null +++ b/internal/store/database/execution.go @@ -0,0 +1,145 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/jmoiron/sqlx" +) + +var _ store.ExecutionStore = (*ExecutionStore)(nil) + +// NewExecutionStore returns a new ExecutionStore. +func NewExecutionStore(db *sqlx.DB) *ExecutionStore { + return &ExecutionStore{db} +} + +// ExecutionStore implements a ExecutionStore backed by a relational +// database. +type ExecutionStore struct { + db *sqlx.DB +} + +// Find finds the execution by id. +func (s *ExecutionStore) Find(ctx context.Context, id int64) (*types.Execution, error) { + dst := new(types.Execution) + err := s.db.Get(dst, executionSelectID, id) + return dst, err +} + +// FindSlug finds the execution by pipeline id and slug. +func (s *ExecutionStore) FindSlug(ctx context.Context, id int64, slug string) (*types.Execution, error) { + dst := new(types.Execution) + err := s.db.Get(dst, executionSelectSlug, id, slug) + return dst, err +} + +// List returns a list of executions. +func (s *ExecutionStore) List(ctx context.Context, id int64, opts types.Params) ([]*types.Execution, error) { + dst := []*types.Execution{} + err := s.db.Select(&dst, executionSelect, id, limit(opts.Size), offset(opts.Page, opts.Size)) + return dst, err +} + +// Create saves the execution details. +func (s *ExecutionStore) Create(ctx context.Context, execution *types.Execution) error { + query, arg, err := s.db.BindNamed(executionInsert, execution) + if err != nil { + return err + } + return s.db.QueryRow(query, arg...).Scan(&execution.ID) +} + +// Update updates the execution details. +func (s *ExecutionStore) Update(ctx context.Context, execution *types.Execution) error { + query, arg, err := s.db.BindNamed(executionUpdate, execution) + if err != nil { + return err + } + _, err = s.db.Exec(query, arg...) + return err +} + +// Delete deletes the execution. +func (s *ExecutionStore) Delete(ctx context.Context, execution *types.Execution) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + // delete the execution + if _, err := tx.Exec(executionDelete, execution.ID); err != nil { + return err + } + return tx.Commit() +} + +const executionBase = ` +SELECT + execution_id +,execution_pipeline_id +,execution_slug +,execution_name +,execution_desc +,execution_created +,execution_updated +FROM executions +` + +const executionSelect = executionBase + ` +WHERE execution_pipeline_id = $1 +ORDER BY execution_name ASC +LIMIT $2 OFFSET $3 +` + +const executionSelectID = executionBase + ` +WHERE execution_id = $1 +` + +const executionSelectSlug = executionBase + ` +WHERE execution_pipeline_id = $1 + AND execution_slug = $2 +` + +const executionInsert = ` +INSERT INTO executions ( + execution_pipeline_id +,execution_slug +,execution_name +,execution_desc +,execution_created +,execution_updated +) values ( + :execution_pipeline_id +,:execution_slug +,:execution_name +,:execution_desc +,:execution_created +,:execution_updated +) RETURNING execution_id +` + +const executionUpdate = ` +UPDATE executions +SET + execution_name = :execution_name +,execution_desc = :execution_desc +,execution_updated = :execution_updated +WHERE execution_id = :execution_id +` + +const executionDelete = ` +DELETE FROM executions +WHERE execution_id = $1 +` + +const executionDeletePipeline = ` +DELETE FROM executions +WHERE execution_pipeline_id = $1 +` diff --git a/internal/store/database/execution_sync.go b/internal/store/database/execution_sync.go new file mode 100644 index 000000000..4677c2cef --- /dev/null +++ b/internal/store/database/execution_sync.go @@ -0,0 +1,67 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/store/database/mutex" + "github.com/bradrydzewski/my-app/types" +) + +var _ store.ExecutionStore = (*ExecutionStoreSync)(nil) + +// NewExecutionStoreSync returns a new ExecutionStoreSync. +func NewExecutionStoreSync(store *ExecutionStore) *ExecutionStoreSync { + return &ExecutionStoreSync{base: store} +} + +// ExecutionStoreSync synronizes read and write access to the +// execution store. This prevents race conditions when the database +// type is sqlite3. +type ExecutionStoreSync struct{ base *ExecutionStore } + +// Find finds the execution by id. +func (s *ExecutionStoreSync) Find(ctx context.Context, id int64) (*types.Execution, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Find(ctx, id) +} + +// FindSlug finds the execution by pipeline id and slug. +func (s *ExecutionStoreSync) FindSlug(ctx context.Context, id int64, slug string) (*types.Execution, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindSlug(ctx, id, slug) +} + +// List returns a list of executions. +func (s *ExecutionStoreSync) List(ctx context.Context, id int64, opts types.Params) ([]*types.Execution, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx, id, opts) +} + +// Create saves the execution details. +func (s *ExecutionStoreSync) Create(ctx context.Context, execution *types.Execution) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Create(ctx, execution) +} + +// Update updates the execution details. +func (s *ExecutionStoreSync) Update(ctx context.Context, execution *types.Execution) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Update(ctx, execution) +} + +// Delete deletes the execution. +func (s *ExecutionStoreSync) Delete(ctx context.Context, execution *types.Execution) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Delete(ctx, execution) +} diff --git a/internal/store/database/execution_test.go b/internal/store/database/execution_test.go new file mode 100644 index 000000000..db4a62dff --- /dev/null +++ b/internal/store/database/execution_test.go @@ -0,0 +1,214 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "database/sql" + "testing" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/jmoiron/sqlx" +) + +// execution fields to ignore in test comparisons +var executionIgnore = cmpopts.IgnoreFields(types.Execution{}, + "ID", "Created", "Updated") + +func TestExecution(t *testing.T) { + db, err := connect() + if err != nil { + t.Error(err) + return + } + defer db.Close() + if err := seed(db); err != nil { + t.Error(err) + return + } + + if _, err := newPipelineStoreSeeded(db); err != nil { + t.Error(err) + return + } + + store := NewExecutionStoreSync(NewExecutionStore(db)) + t.Run("create", testExecutionCreate(store)) + t.Run("find", testExecutionFind(store)) + t.Run("list", testExecutionList(store)) + t.Run("update", testExecutionUpdate(store)) + t.Run("delete", testExecutionDelete(store)) +} + +// this test creates entries in the database and confirms +// the primary keys were auto-incremented. +func testExecutionCreate(store store.ExecutionStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.Execution{} + if err := unmarshal("testdata/executions.json", &vv); err != nil { + t.Error(err) + return + } + + // create row 1 + v := vv[0] + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + if v.ID == 0 { + t.Errorf("Want autoincremented primary key") + } + // create row 2 + v = vv[1] + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + // create row 3 + v = vv[2] + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + + t.Run("duplicate slug", func(t *testing.T) { + // reset the ID so that a new row is created + // using the same slug + v.ID = 0 + if err := store.Create(noContext, v); err == nil { + t.Errorf("Expect duplicate row error") + return + } + }) + } +} + +// this test fetches executions from the database by id and key +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testExecutionFind(store store.ExecutionStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.Execution{} + if err := unmarshal("testdata/executions.json", &vv); err != nil { + t.Error(err) + return + } + want := vv[0] + + t.Run("id", func(t *testing.T) { + got, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, executionIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("slug", func(t *testing.T) { + got, err := store.FindSlug(noContext, want.Pipeline, want.Slug) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, executionIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + } +} + +// this test fetches a list of executions from the database +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testExecutionList(store store.ExecutionStore) func(t *testing.T) { + return func(t *testing.T) { + want := []*types.Execution{} + if err := unmarshal("testdata/executions.json", &want); err != nil { + t.Error(err) + return + } + got, err := store.List(noContext, 2, types.Params{Size: 25, Page: 0}) + if err != nil { + t.Error(err) + return + } + + if diff := cmp.Diff(got, want[1:], executionIgnore); len(diff) != 0 { + t.Errorf(diff) + debug(t, got) + return + } + } +} + +// this test updates a execution in the database and then fetches +// the execution and confirms the column was updated as expected. +func testExecutionUpdate(store store.ExecutionStore) func(t *testing.T) { + return func(t *testing.T) { + before, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + before.Desc = "updated description" + if err := store.Update(noContext, before); err != nil { + t.Error(err) + return + } + after, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + + if diff := cmp.Diff(before, after, executionIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + } +} + +// this test deletes a execution from the database and then confirms +// subsequent attempts to fetch the deleted execution result in +// a sql.ErrNoRows error. +func testExecutionDelete(store store.ExecutionStore) func(t *testing.T) { + return func(t *testing.T) { + v, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if err := store.Delete(noContext, v); err != nil { + t.Error(err) + return + } + if _, err := store.Find(noContext, 1); err != sql.ErrNoRows { + t.Errorf("Expected sql.ErrNoRows got %s", err) + } + } +} + +// helper function that returns an execution store that is seeded +// with execution data loaded from a json file. +func newExecutionStoreSeeded(db *sqlx.DB) (store.ExecutionStore, error) { + store := NewExecutionStoreSync(NewExecutionStore(db)) + vv := []*types.Execution{} + if err := unmarshal("testdata/executions.json", &vv); err != nil { + return nil, err + } + for _, v := range vv { + if err := store.Create(noContext, v); err != nil { + return nil, err + } + } + return store, nil +} diff --git a/internal/store/database/migrate/migrate.go b/internal/store/database/migrate/migrate.go new file mode 100644 index 000000000..56f317ad2 --- /dev/null +++ b/internal/store/database/migrate/migrate.go @@ -0,0 +1,58 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package migrate + +import ( + "context" + "database/sql" + "embed" + "io/fs" + + "github.com/jmoiron/sqlx" + "github.com/maragudk/migrate" + "github.com/rs/zerolog/log" +) + +// background context +var noContext = context.Background() + +//go:embed postgres/*.sql +var postgres embed.FS + +//go:embed sqlite/*.sql +var sqlite embed.FS + +// Migrate performs the database migration. +func Migrate(db *sqlx.DB) error { + before := func(_ context.Context, _ *sql.Tx, version string) error { + log.Trace().Str("version", version).Msg("migration started") + return nil + } + + after := func(_ context.Context, _ *sql.Tx, version string) error { + log.Trace().Str("version", version).Msg("migration complete") + return nil + } + + opts := migrate.Options{ + After: after, + Before: before, + DB: db.DB, + FS: sqlite, + Table: "migrations", + } + + switch db.DriverName() { + case "postgres": + folder, _ := fs.Sub(postgres, "postgres") + opts.FS = folder + + default: + folder, _ := fs.Sub(sqlite, "sqlite") + opts.FS = folder + } + + return migrate.New(opts).MigrateUp(noContext) +} diff --git a/internal/store/database/migrate/postgres/0000_create_extension_btree.up.sql b/internal/store/database/migrate/postgres/0000_create_extension_btree.up.sql new file mode 100644 index 000000000..ee2f06101 --- /dev/null +++ b/internal/store/database/migrate/postgres/0000_create_extension_btree.up.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS btree_gin; diff --git a/internal/store/database/migrate/postgres/0000_create_extension_citext.up.sql b/internal/store/database/migrate/postgres/0000_create_extension_citext.up.sql new file mode 100644 index 000000000..1f9a5441f --- /dev/null +++ b/internal/store/database/migrate/postgres/0000_create_extension_citext.up.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS citext; \ No newline at end of file diff --git a/internal/store/database/migrate/postgres/0000_create_extension_trgm.up.sql b/internal/store/database/migrate/postgres/0000_create_extension_trgm.up.sql new file mode 100644 index 000000000..588aec00f --- /dev/null +++ b/internal/store/database/migrate/postgres/0000_create_extension_trgm.up.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/internal/store/database/migrate/postgres/0001_create_table_executions.up.sql b/internal/store/database/migrate/postgres/0001_create_table_executions.up.sql new file mode 100644 index 000000000..5da80a3e6 --- /dev/null +++ b/internal/store/database/migrate/postgres/0001_create_table_executions.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS executions ( + execution_id SERIAL PRIMARY KEY +,execution_pipeline_id INTEGER +,execution_slug TEXT +,execution_name TEXT +,execution_desc TEXT +,execution_created INTEGER +,execution_updated INTEGER +,UNIQUE(execution_pipeline_id, execution_slug) +); diff --git a/internal/store/database/migrate/postgres/0001_create_table_pipelines.up.sql b/internal/store/database/migrate/postgres/0001_create_table_pipelines.up.sql new file mode 100644 index 000000000..acd8eaa0f --- /dev/null +++ b/internal/store/database/migrate/postgres/0001_create_table_pipelines.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS pipelines ( + pipeline_id SERIAL PRIMARY KEY +,pipeline_name TEXT +,pipeline_slug TEXT +,pipeline_desc TEXT +,pipeline_token TEXT +,pipeline_active BOOLEAN +,pipeline_created INTEGER +,pipeline_updated INTEGER +,UNIQUE(pipeline_token) +,UNIQUE(pipeline_slug) +); diff --git a/internal/store/database/migrate/postgres/0001_create_table_users.up.sql b/internal/store/database/migrate/postgres/0001_create_table_users.up.sql new file mode 100644 index 000000000..3517c04d5 --- /dev/null +++ b/internal/store/database/migrate/postgres/0001_create_table_users.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS users ( + user_id SERIAL PRIMARY KEY +,user_email CITEXT +,user_password TEXT +,user_salt TEXT +,user_name TEXT +,user_company TEXT +,user_admin BOOLEAN +,user_blocked BOOLEAN +,user_created INTEGER +,user_updated INTEGER +,user_authed INTEGER +,UNIQUE(user_salt) +,UNIQUE(user_email) +); diff --git a/internal/store/database/migrate/postgres/0002_create_index_executions_pipeline.up.sql b/internal/store/database/migrate/postgres/0002_create_index_executions_pipeline.up.sql new file mode 100644 index 000000000..fa8808fe8 --- /dev/null +++ b/internal/store/database/migrate/postgres/0002_create_index_executions_pipeline.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS index_execution_pipeline +ON executions(execution_pipeline_id); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_executions.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_executions.up.sql new file mode 100644 index 000000000..e1e551384 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0001_create_table_executions.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS executions ( + execution_id INTEGER PRIMARY KEY AUTOINCREMENT +,execution_pipeline_id INTEGER +,execution_slug TEXT COLLATE NOCASE +,execution_name TEXT +,execution_desc TEXT +,execution_created INTEGER +,execution_updated INTEGER +,UNIQUE(execution_pipeline_id, execution_slug) +); diff --git a/internal/store/database/migrate/sqlite/0001_create_table_pipelines.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_pipelines.up.sql new file mode 100644 index 000000000..25d21c46a --- /dev/null +++ b/internal/store/database/migrate/sqlite/0001_create_table_pipelines.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS pipelines ( + pipeline_id INTEGER PRIMARY KEY AUTOINCREMENT +,pipeline_name TEXT +,pipeline_slug TEXT COLLATE NOCASE +,pipeline_desc TEXT +,pipeline_token TEXT +,pipeline_active BOOLEAN +,pipeline_created INTEGER +,pipeline_updated INTEGER +,UNIQUE(pipeline_token) +,UNIQUE(pipeline_slug COLLATE NOCASE) +); \ No newline at end of file diff --git a/internal/store/database/migrate/sqlite/0001_create_table_users.up.sql b/internal/store/database/migrate/sqlite/0001_create_table_users.up.sql new file mode 100644 index 000000000..8190463b3 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0001_create_table_users.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT +,user_email TEXT COLLATE NOCASE +,user_password TEXT +,user_salt TEXT +,user_name TEXT +,user_company TEXT +,user_admin BOOLEAN +,user_blocked BOOLEAN +,user_created INTEGER +,user_updated INTEGER +,user_authed INTEGER +,UNIQUE(user_salt) +,UNIQUE(user_email COLLATE NOCASE) +); diff --git a/internal/store/database/migrate/sqlite/0002_create_index_executions_pipeline.up.sql b/internal/store/database/migrate/sqlite/0002_create_index_executions_pipeline.up.sql new file mode 100644 index 000000000..fa8808fe8 --- /dev/null +++ b/internal/store/database/migrate/sqlite/0002_create_index_executions_pipeline.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS index_execution_pipeline +ON executions(execution_pipeline_id); diff --git a/internal/store/database/mutex/mutex.go b/internal/store/database/mutex/mutex.go new file mode 100644 index 000000000..3f1b7aa44 --- /dev/null +++ b/internal/store/database/mutex/mutex.go @@ -0,0 +1,22 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package mutex provides a global mutex. +package mutex + +import "sync" + +var m sync.RWMutex + +// RLock locks the global mutex for reads. +func RLock() { m.RLock() } + +// RUnlock unlocks the global mutex. +func RUnlock() { m.RUnlock() } + +// Lock locks the global mutex for writes. +func Lock() { m.Lock() } + +// Unlock unlocks the global mutex. +func Unlock() { m.Unlock() } diff --git a/internal/store/database/pipeline.go b/internal/store/database/pipeline.go new file mode 100644 index 000000000..1cb10137b --- /dev/null +++ b/internal/store/database/pipeline.go @@ -0,0 +1,167 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/jmoiron/sqlx" +) + +var _ store.PipelineStore = (*PipelineStore)(nil) + +// NewPipelineStore returns a new PipelinetStore. +func NewPipelineStore(db *sqlx.DB) *PipelineStore { + return &PipelineStore{db} +} + +// PipelineStore implements a PipelineStore backed by a +// relational database. +type PipelineStore struct { + db *sqlx.DB +} + +// Find finds the pipeline by id. +func (s *PipelineStore) Find(ctx context.Context, id int64) (*types.Pipeline, error) { + dst := new(types.Pipeline) + err := s.db.Get(dst, pipelineSelectID, id) + return dst, err +} + +// FindToken finds the pipeline by token. +func (s *PipelineStore) FindToken(ctx context.Context, token string) (*types.Pipeline, error) { + dst := new(types.Pipeline) + err := s.db.Get(dst, pipelineSelectToken, token) + return dst, err +} + +// FindSlug finds the pipeline by slug. +func (s *PipelineStore) FindSlug(ctx context.Context, slug string) (*types.Pipeline, error) { + dst := new(types.Pipeline) + err := s.db.Get(dst, pipelineSelectSlug, slug) + return dst, err +} + +// List returns a list of pipelines by user. +func (s *PipelineStore) List(ctx context.Context, user int64, opts types.Params) ([]*types.Pipeline, error) { + dst := []*types.Pipeline{} + err := s.db.Select(&dst, pipelineSelect, limit(opts.Size), offset(opts.Page, opts.Size)) + return dst, err +} + +// Create saves the pipeline details. +func (s *PipelineStore) Create(ctx context.Context, pipeline *types.Pipeline) error { + query, arg, err := s.db.BindNamed(pipelineInsert, pipeline) + if err != nil { + return err + } + return s.db.QueryRow(query, arg...).Scan(&pipeline.ID) +} + +// Update updates the pipeline details. +func (s *PipelineStore) Update(ctx context.Context, pipeline *types.Pipeline) error { + query, arg, err := s.db.BindNamed(pipelineUpdate, pipeline) + if err != nil { + return err + } + _, err = s.db.Exec(query, arg...) + return err +} + +// Delete deletes the pipeline. +func (s *PipelineStore) Delete(ctx context.Context, pipeline *types.Pipeline) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // pleae note that we are aware of foreign keys and + // cascading deletes, however, we chose to implement + // this logic in the application code in the event we + // want to leverage citus postgres. + // + // to future developers: feel free to remove and + // replace with foreign keys and cascading deletes + // at your discretion. + + // delete the executions associated with the pipeline + if _, err := tx.Exec(executionDeletePipeline, pipeline.ID); err != nil { + return err + } + // delete the pipeline + if _, err := tx.Exec(pipelineDelete, pipeline.ID); err != nil { + return err + } + return tx.Commit() +} + +const pipelineBase = ` +SELECT + pipeline_id +,pipeline_name +,pipeline_slug +,pipeline_desc +,pipeline_token +,pipeline_active +,pipeline_created +,pipeline_updated +FROM pipelines +` + +const pipelineSelect = pipelineBase + ` +ORDER BY pipeline_slug +LIMIT $1 OFFSET $2 +` + +const pipelineSelectID = pipelineBase + ` +WHERE pipeline_id = $1 +` + +const pipelineSelectToken = pipelineBase + ` +WHERE pipeline_token = $1 +` + +const pipelineSelectSlug = pipelineBase + ` +WHERE pipeline_slug = $1 +` + +const pipelineDelete = ` +DELETE FROM pipelines +WHERE pipeline_id = $1 +` + +const pipelineInsert = ` +INSERT INTO pipelines ( + pipeline_name +,pipeline_slug +,pipeline_desc +,pipeline_token +,pipeline_active +,pipeline_created +,pipeline_updated +) values ( + :pipeline_name +,:pipeline_slug +,:pipeline_desc +,:pipeline_token +,:pipeline_active +,:pipeline_created +,:pipeline_updated +) RETURNING pipeline_id +` + +const pipelineUpdate = ` +UPDATE pipelines +SET + pipeline_name = :pipeline_name +,pipeline_desc = :pipeline_desc +,pipeline_active = :pipeline_active +,pipeline_updated = :pipeline_updated +WHERE pipeline_id = :pipeline_id +` diff --git a/internal/store/database/pipeline_sync.go b/internal/store/database/pipeline_sync.go new file mode 100644 index 000000000..68d975375 --- /dev/null +++ b/internal/store/database/pipeline_sync.go @@ -0,0 +1,74 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/store/database/mutex" + "github.com/bradrydzewski/my-app/types" +) + +var _ store.PipelineStore = (*PipelineStoreSync)(nil) + +// NewPipelineStoreSync returns a new PipelineStoreSync. +func NewPipelineStoreSync(store *PipelineStore) *PipelineStoreSync { + return &PipelineStoreSync{base: store} +} + +// PipelineStoreSync synronizes read and write access to the +// pipeline store. This prevents race conditions when the database +// type is sqlite3. +type PipelineStoreSync struct{ base *PipelineStore } + +// Find finds the pipeline by id. +func (s *PipelineStoreSync) Find(ctx context.Context, id int64) (*types.Pipeline, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Find(ctx, id) +} + +// FindToken finds the pipeline by token. +func (s *PipelineStoreSync) FindToken(ctx context.Context, token string) (*types.Pipeline, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindToken(ctx, token) +} + +// FindSlug finds the pipeline by slug. +func (s *PipelineStoreSync) FindSlug(ctx context.Context, slug string) (*types.Pipeline, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindSlug(ctx, slug) +} + +// List returns a list of pipelines by user. +func (s *PipelineStoreSync) List(ctx context.Context, id int64, opts types.Params) ([]*types.Pipeline, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx, id, opts) +} + +// Create saves the pipeline details. +func (s *PipelineStoreSync) Create(ctx context.Context, pipeline *types.Pipeline) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Create(ctx, pipeline) +} + +// Update updates the pipeline details. +func (s *PipelineStoreSync) Update(ctx context.Context, pipeline *types.Pipeline) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Update(ctx, pipeline) +} + +// Delete deletes the pipeline. +func (s *PipelineStoreSync) Delete(ctx context.Context, pipeline *types.Pipeline) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Delete(ctx, pipeline) +} diff --git a/internal/store/database/pipeline_test.go b/internal/store/database/pipeline_test.go new file mode 100644 index 000000000..9f0abc04d --- /dev/null +++ b/internal/store/database/pipeline_test.go @@ -0,0 +1,254 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "database/sql" + "fmt" + "testing" + "time" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/jmoiron/sqlx" +) + +// pipeline fields to ignore in test comparisons +var pipelineIgnore = cmpopts.IgnoreFields(types.Pipeline{}, + "ID", "Token", "Created", "Updated") + +func TestPipeline(t *testing.T) { + db, err := connect() + if err != nil { + t.Error(err) + return + } + defer db.Close() + if err := seed(db); err != nil { + t.Error(err) + return + } + + if _, err := newUserStoreSeeded(db); err != nil { + t.Error(err) + return + } + + store := NewPipelineStoreSync(NewPipelineStore(db)) + t.Run("create", testPipelineCreate(store)) + t.Run("find", testPipelineFind(store)) + t.Run("list", testPipelineList(store)) + t.Run("update", testPipelineUpdate(store)) + t.Run("delete", testPipelineDelete(store)) +} + +// this test creates entries in the database and confirms +// the primary keys were auto-incremented. +func testPipelineCreate(store store.PipelineStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.Pipeline{} + if err := unmarshal("testdata/pipelines.json", &vv); err != nil { + t.Error(err) + return + } + // create row 1 + v := vv[0] + // generate a deterministic token for each + // entry based on the hash of the email. + v.Token = fmt.Sprintf("%x", v.Slug) + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + if v.ID == 0 { + t.Errorf("Want autoincremented primary key") + } + // create row 2 + v = vv[1] + v.Token = fmt.Sprintf("%x", v.Slug) + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + if v.ID == 0 { + t.Errorf("Want autoincremented primary key") + } + + t.Run("duplicate slug", func(t *testing.T) { + v.ID = 0 + v.Token = "9afeab83324a53" + v.Slug = "cassini" + if err := store.Create(noContext, v); err == nil { + t.Errorf("Expect duplicate row error") + return + } + }) + + t.Run("duplicate token", func(t *testing.T) { + v.ID = 0 + v.Slug = "voyager" + v.Token = "63617373696e69" + if err := store.Create(noContext, v); err == nil { + t.Errorf("Expect duplicate row error") + return + } + }) + } +} + +// this test fetches pipelines from the database by id and key +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testPipelineFind(store store.PipelineStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.Pipeline{} + if err := unmarshal("testdata/pipelines.json", &vv); err != nil { + t.Error(err) + return + } + want := vv[0] + want.Token = "63617373696e69" + + // Find row by ID + got, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + + t.Run("token", func(t *testing.T) { + got, err := store.FindToken(noContext, want.Token) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("slug", func(t *testing.T) { + got, err := store.FindSlug(noContext, want.Slug) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("slug", func(t *testing.T) { + got, err := store.FindSlug(noContext, want.Slug) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + } +} + +// this test fetches a list of pipelines from the database +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testPipelineList(store store.PipelineStore) func(t *testing.T) { + return func(t *testing.T) { + want := []*types.Pipeline{} + if err := unmarshal("testdata/pipelines.json", &want); err != nil { + t.Error(err) + return + } + got, err := store.List(noContext, 2, types.Params{Page: 0, Size: 100}) + if err != nil { + t.Error(err) + return + } + if len(got) != 2 { + t.Errorf("Expect 2 pipelines") + } + if diff := cmp.Diff(got, want, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + } +} + +// this test updates an pipeline in the database and then fetches +// the pipeline and confirms the column was updated as expected. +func testPipelineUpdate(store store.PipelineStore) func(t *testing.T) { + return func(t *testing.T) { + before, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + before.Updated = time.Now().Unix() + before.Active = false + if err := store.Update(noContext, before); err != nil { + t.Error(err) + return + } + after, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + + if diff := cmp.Diff(before, after, pipelineIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + } +} + +// this test deletes an pipeline from the database and then confirms +// subsequent attempts to fetch the deleted pipeline result in +// a sql.ErrNoRows error. +func testPipelineDelete(store store.PipelineStore) func(t *testing.T) { + return func(t *testing.T) { + v, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if err := store.Delete(noContext, v); err != nil { + t.Error(err) + return + } + if _, err := store.Find(noContext, 1); err != sql.ErrNoRows { + t.Errorf("Expected sql.ErrNoRows got %s", err) + } + } +} + +// helper function that returns an pipeline store that is seeded +// with pipeline data loaded from a json file. +func newPipelineStoreSeeded(db *sqlx.DB) (store.PipelineStore, error) { + store := NewPipelineStoreSync(NewPipelineStore(db)) + vv := []*types.Pipeline{} + if err := unmarshal("testdata/pipelines.json", &vv); err != nil { + return nil, err + } + for _, v := range vv { + v.Token = fmt.Sprintf("%x", v.Slug) + if err := store.Create(noContext, v); err != nil { + return nil, err + } + } + return store, nil +} diff --git a/internal/store/database/store.go b/internal/store/database/store.go new file mode 100644 index 000000000..1067cabbf --- /dev/null +++ b/internal/store/database/store.go @@ -0,0 +1,66 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package database provides persistent data storage using +// a postgres or sqlite3 database. +package database + +import ( + "database/sql" + "time" + + "github.com/bradrydzewski/my-app/internal/store/database/migrate" + + "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" +) + +// build is a global instance of the sql builder. we are able to +// hardcode to postgres since sqlite3 is compatible with postgres. +var builder = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + +// Connect to a database and verify with a ping. +func Connect(driver, datasource string) (*sqlx.DB, error) { + db, err := sql.Open(driver, datasource) + if err != nil { + return nil, err + } + dbx := sqlx.NewDb(db, driver) + if err := pingDatabase(dbx); err != nil { + return nil, err + } + if err := setupDatabase(dbx); err != nil { + return nil, err + } + return dbx, nil +} + +// Must is a helper function that wraps a call to Connect +// and panics if the error is non-nil. +func Must(db *sqlx.DB, err error) *sqlx.DB { + if err != nil { + panic(err) + } + return db +} + +// helper function to ping the database with backoff to ensure +// a connection can be established before we proceed with the +// database setup and migration. +func pingDatabase(db *sqlx.DB) (err error) { + for i := 0; i < 30; i++ { + err = db.Ping() + if err == nil { + return + } + time.Sleep(time.Second) + } + return +} + +// helper function to setup the databsae by performing automated +// database migration steps. +func setupDatabase(db *sqlx.DB) error { + return migrate.Migrate(db) +} diff --git a/internal/store/database/store_test.go b/internal/store/database/store_test.go new file mode 100644 index 000000000..9e218aa53 --- /dev/null +++ b/internal/store/database/store_test.go @@ -0,0 +1,63 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/jmoiron/sqlx" + + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +var noContext = context.Background() + +// connect opens a new test database connection. +func connect() (*sqlx.DB, error) { + var ( + driver = "sqlite3" + config = ":memory:?_foreign_keys=1" + ) + if os.Getenv("DATABASE_CONFIG") != "" { + driver = os.Getenv("DATABASE_DRIVER") + config = os.Getenv("DATABASE_CONFIG") + } + return Connect(driver, config) +} + +// seed seed the database state. +func seed(db *sqlx.DB) error { + db.Exec("DELETE FROM executions") + db.Exec("DELETE FROM pipelines") + db.Exec("DELETE FROM users") + db.Exec("ALTER SEQUENCE users_user_id_seq RESTART WITH 1") + db.Exec("ALTER SEQUENCE pipelines_pipeline_id_seq RESTART WITH 1") + db.Exec("ALTER SEQUENCE executions_execution_id_seq RESTART WITH 1") + return nil +} + +// unmarshal a testdata file. +func unmarshal(path string, v interface{}) error { + out, err := ioutil.ReadFile(path) + if err != nil { + return err + } + return json.Unmarshal(out, v) +} + +// dump json data to the test logs. +func debug(t *testing.T, v interface{}) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.Encode(v) + t.Log(buf.String()) +} diff --git a/internal/store/database/testdata/executions.json b/internal/store/database/testdata/executions.json new file mode 100644 index 000000000..affbb729a --- /dev/null +++ b/internal/store/database/testdata/executions.json @@ -0,0 +1,30 @@ +[ + { + "id": 0, + "pipeline": 1, + "slug": "foo", + "name": "Foo", + "desc": "this is a foo", + "token": "63617373696e69", + "created": 1637192718, + "updated": 1637192725 + }, + { + "id": 0, + "pipeline": 2, + "slug": "bar", + "name": "Bar", + "desc": "this is a bar", + "created": 1637192718, + "updated": 1637192725 + }, + { + "id": 0, + "pipeline": 2, + "slug": "baz", + "name": "Baz", + "desc": "this is a baz", + "created": 1637192718, + "updated": 1637192725 + } +] \ No newline at end of file diff --git a/internal/store/database/testdata/pipelines.json b/internal/store/database/testdata/pipelines.json new file mode 100644 index 000000000..f34dddeb4 --- /dev/null +++ b/internal/store/database/testdata/pipelines.json @@ -0,0 +1,22 @@ +[ + { + "id": 0, + "slug": "cassini", + "name": "Cassini", + "desc": "orbit saturn and its moons", + "token": "63617373696e69", + "archived": true, + "created": 0, + "updated": 0 + }, + { + "id": 0, + "slug": "galileo", + "name": "Galileo", + "desc": "orbit jupiter and its moons", + "token": "67616c696c656f", + "archived": true, + "created": 0, + "updated": 0 + } +] \ No newline at end of file diff --git a/internal/store/database/testdata/users.json b/internal/store/database/testdata/users.json new file mode 100644 index 000000000..e2b20bd77 --- /dev/null +++ b/internal/store/database/testdata/users.json @@ -0,0 +1,24 @@ +[ + { + "id": 0, + "email": "jane@example.com", + "name": "jane", + "company": "acme", + "admin": true, + "blocked": false, + "created": 0, + "updated": 0, + "authed": 0 + }, + { + "id": 0, + "email": "john@example.com", + "name": "john", + "company": "acme", + "admin": false, + "blocked": false, + "created": 0, + "updated": 0, + "authed": 0 + } +] \ No newline at end of file diff --git a/internal/store/database/user.go b/internal/store/database/user.go new file mode 100644 index 000000000..2c8ec131d --- /dev/null +++ b/internal/store/database/user.go @@ -0,0 +1,217 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "strconv" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + "github.com/bradrydzewski/my-app/types/enum" + + "github.com/jmoiron/sqlx" +) + +var _ store.UserStore = (*UserStore)(nil) + +// NewUserStore returns a new UserStore. +func NewUserStore(db *sqlx.DB) *UserStore { + return &UserStore{db} +} + +// UserStore implements a UserStore backed by a relational +// database. +type UserStore struct { + db *sqlx.DB +} + +// Find finds the user by id. +func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) { + dst := new(types.User) + err := s.db.Get(dst, userSelectID, id) + return dst, err +} + +// FindEmail finds the user by email. +func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) { + dst := new(types.User) + err := s.db.Get(dst, userSelectEmail, email) + return dst, err +} + +// FindKey finds the user unique key (email or id). +func (s *UserStore) FindKey(ctx context.Context, key string) (*types.User, error) { + id, err := strconv.ParseInt(key, 10, 64) + if err == nil { + return s.Find(ctx, id) + } else { + return s.FindEmail(ctx, key) + } +} + +// List returns a list of users. +func (s *UserStore) List(ctx context.Context, opts types.UserFilter) ([]*types.User, error) { + dst := []*types.User{} + + // if the user does not provide any customer filter + // or sorting we use the default select statement. + if opts.Sort == enum.UserAttrNone { + err := s.db.Select(&dst, userSelect, limit(opts.Size), offset(opts.Page, opts.Size)) + return dst, err + } + + // else we construct the sql statement. + stmt := builder.Select("*").From("users") + stmt = stmt.Limit(uint64(limit(opts.Size))) + stmt = stmt.Offset(uint64(offset(opts.Page, opts.Size))) + + switch opts.Sort { + case enum.UserAttrCreated: + // NOTE: string concatination is safe because the + // order attribute is an enum and is not user-defined, + // and is therefore not subject to injection attacks. + stmt = stmt.OrderBy("user_id " + opts.Order.String()) + case enum.UserAttrUpdated: + stmt = stmt.OrderBy("user_updated " + opts.Order.String()) + case enum.UserAttrEmail: + stmt = stmt.OrderBy("user_email " + opts.Order.String()) + case enum.UserAttrId: + stmt = stmt.OrderBy("user_id " + opts.Order.String()) + } + + sql, _, err := stmt.ToSql() + if err != nil { + return dst, err + } + + err = s.db.Select(&dst, sql) + return dst, err +} + +// Create saves the user details. +func (s *UserStore) Create(ctx context.Context, user *types.User) error { + query, arg, err := s.db.BindNamed(userInsert, user) + if err != nil { + return err + } + return s.db.QueryRow(query, arg...).Scan(&user.ID) +} + +// Update updates the user details. +func (s *UserStore) Update(ctx context.Context, user *types.User) error { + query, arg, err := s.db.BindNamed(userUpdate, user) + if err != nil { + return err + } + _, err = s.db.Exec(query, arg...) + return err +} + +// Delete deletes the user. +func (s *UserStore) Delete(ctx context.Context, user *types.User) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + // delete the user + if _, err := tx.Exec(userDelete, user.ID); err != nil { + return err + } + return tx.Commit() +} + +// Count returns a count of users. +func (s *UserStore) Count(context.Context) (int64, error) { + var count int64 + err := s.db.QueryRow(userCount).Scan(&count) + return count, err +} + +const userCount = ` +SELECT count(*) +FROM users +` + +const userBase = ` +SELECT + user_id +,user_email +,user_name +,user_company +,user_password +,user_salt +,user_admin +,user_blocked +,user_created +,user_updated +,user_authed +FROM users +` + +const userSelect = userBase + ` +ORDER BY user_email ASC +LIMIT $1 OFFSET $2 +` + +const userSelectID = userBase + ` +WHERE user_id = $1 +` + +const userSelectEmail = userBase + ` +WHERE user_email = $1 +` + +const userSelectToken = userBase + ` +WHERE user_salt = $1 +` + +const userDelete = ` +DELETE FROM users +WHERE user_id = $1 +` + +const userInsert = ` +INSERT INTO users ( + user_email +,user_name +,user_company +,user_password +,user_salt +,user_admin +,user_blocked +,user_created +,user_updated +,user_authed +) values ( + :user_email +,:user_name +,:user_company +,:user_password +,:user_salt +,:user_admin +,:user_blocked +,:user_created +,:user_updated +,:user_authed +) RETURNING user_id +` + +const userUpdate = ` +UPDATE users +SET + user_email = :user_email +,user_name = :user_name +,user_company = :user_company +,user_password = :user_password +,user_salt = :user_salt +,user_admin = :user_admin +,user_blocked = :user_blocked +,user_created = :user_created +,user_updated = :user_updated +,user_authed = :user_authed +WHERE user_id = :user_id +` diff --git a/internal/store/database/user_sync.go b/internal/store/database/user_sync.go new file mode 100644 index 000000000..236957b51 --- /dev/null +++ b/internal/store/database/user_sync.go @@ -0,0 +1,81 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/internal/store/database/mutex" + "github.com/bradrydzewski/my-app/types" +) + +var _ store.UserStore = (*UserStoreSync)(nil) + +// NewUserStoreSync returns a new UserStoreSync. +func NewUserStoreSync(store *UserStore) *UserStoreSync { + return &UserStoreSync{base: store} +} + +// UserStoreSync synronizes read and write access to the +// user store. This prevents race conditions when the database +// type is sqlite3. +type UserStoreSync struct{ base *UserStore } + +// Find finds the user by id. +func (s *UserStoreSync) Find(ctx context.Context, id int64) (*types.User, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.Find(ctx, id) +} + +// FindEmail finds the user by email. +func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.User, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindEmail(ctx, email) +} + +// FindKey finds the user unique key (email or id). +func (s *UserStoreSync) FindKey(ctx context.Context, key string) (*types.User, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.FindKey(ctx, key) +} + +// List returns a list of users. +func (s *UserStoreSync) List(ctx context.Context, opts types.UserFilter) ([]*types.User, error) { + mutex.RLock() + defer mutex.RUnlock() + return s.base.List(ctx, opts) +} + +// Create saves the user details. +func (s *UserStoreSync) Create(ctx context.Context, user *types.User) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Create(ctx, user) +} + +// Update updates the user details. +func (s *UserStoreSync) Update(ctx context.Context, user *types.User) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Update(ctx, user) +} + +// Delete deletes the user. +func (s *UserStoreSync) Delete(ctx context.Context, user *types.User) error { + mutex.Lock() + defer mutex.Unlock() + return s.base.Delete(ctx, user) +} + +// Count returns a count of users. +func (s *UserStoreSync) Count(ctx context.Context) (int64, error) { + mutex.Lock() + defer mutex.Unlock() + return s.base.Count(ctx) +} diff --git a/internal/store/database/user_test.go b/internal/store/database/user_test.go new file mode 100644 index 000000000..b7a621888 --- /dev/null +++ b/internal/store/database/user_test.go @@ -0,0 +1,272 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "database/sql" + "fmt" + "strings" + "testing" + "time" + + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/jmoiron/sqlx" +) + +// user fields to ignore in test comparisons +var userIgnore = cmpopts.IgnoreFields(types.User{}, + "ID", "Salt", "Created", "Updated") + +func TestUser(t *testing.T) { + db, err := connect() + if err != nil { + t.Error(err) + return + } + defer db.Close() + if err := seed(db); err != nil { + t.Error(err) + return + } + + store := NewUserStoreSync(NewUserStore(db)) + t.Run("create", testUserCreate(store)) + t.Run("duplicate", testUserDuplicate(store)) + t.Run("count", testUserCount(store)) + t.Run("find", testUserFind(store)) + t.Run("list", testUserList(store)) + t.Run("update", testUserUpdate(store)) + t.Run("delete", testUserDelete(store)) +} + +// this test creates entries in the database and confirms +// the primary keys were auto-incremented. +func testUserCreate(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.User{} + if err := unmarshal("testdata/users.json", &vv); err != nil { + t.Error(err) + return + } + // create row 1 + v := vv[0] + // generate a deterministic token for each + // entry based on the hash of the email. + v.Salt = fmt.Sprintf("%x", v.Email) + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + if v.ID == 0 { + t.Errorf("Want autoincremented primary key") + } + // create row 2 + v = vv[1] + v.Salt = fmt.Sprintf("%x", v.Email) + if err := store.Create(noContext, v); err != nil { + t.Error(err) + return + } + if v.ID == 0 { + t.Errorf("Want autoincremented primary key") + } + } +} + +// this test attempts to create an entry in the database using +// a duplicate email to verify that unique email constraints are +// being enforced. +func testUserDuplicate(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.User{} + if err := unmarshal("testdata/users.json", &vv); err != nil { + t.Error(err) + return + } + if err := store.Create(noContext, vv[0]); err == nil { + t.Errorf("Expect unique index violation") + } + } +} + +// this test counts the number of users in the database +// and compares to the expected count. +func testUserCount(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + got, err := store.Count(noContext) + if err != nil { + t.Error(err) + return + } + if want := int64(2); got != want { + t.Errorf("Want user count %d, got %d", want, got) + } + } +} + +// this test fetches users from the database by id and key +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testUserFind(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + vv := []*types.User{} + if err := unmarshal("testdata/users.json", &vv); err != nil { + t.Error(err) + return + } + want := vv[0] + + t.Run("id", func(t *testing.T) { + got, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("email", func(t *testing.T) { + got, err := store.FindEmail(noContext, want.Email) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("email/nocase", func(t *testing.T) { + got, err := store.FindEmail(noContext, strings.ToUpper(want.Email)) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("key/id", func(t *testing.T) { + got, err := store.FindKey(noContext, "1") + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + + t.Run("key/email", func(t *testing.T) { + got, err := store.FindKey(noContext, want.Email) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + }) + } +} + +// this test fetches a list of users from the database +// and compares to the expected results (sourced from a json file) +// to ensure all columns are correctly mapped. +func testUserList(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + want := []*types.User{} + if err := unmarshal("testdata/users.json", &want); err != nil { + t.Error(err) + return + } + got, err := store.List(noContext, types.UserFilter{Page: 0, Size: 100}) + if err != nil { + t.Error(err) + return + } + if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + } +} + +// this test updates an user in the database and then fetches +// the user and confirms the column was updated as expected. +func testUserUpdate(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + before, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + before.Updated = time.Now().Unix() + before.Authed = time.Now().Unix() + if err := store.Update(noContext, before); err != nil { + t.Error(err) + return + } + after, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + + if diff := cmp.Diff(before, after, userIgnore); len(diff) != 0 { + t.Errorf(diff) + return + } + } +} + +// this test deletes an user from the database and then confirms +// subsequent attempts to fetch the deleted user result in +// a sql.ErrNoRows error. +func testUserDelete(store store.UserStore) func(t *testing.T) { + return func(t *testing.T) { + v, err := store.Find(noContext, 1) + if err != nil { + t.Error(err) + return + } + if err := store.Delete(noContext, v); err != nil { + t.Error(err) + return + } + if _, err := store.Find(noContext, 1); err != sql.ErrNoRows { + t.Errorf("Expected sql.ErrNoRows got %s", err) + } + } +} + +// helper function that returns an user store that is seeded +// with user data loaded from a json file. +func newUserStoreSeeded(db *sqlx.DB) (store.UserStore, error) { + store := NewUserStoreSync(NewUserStore(db)) + vv := []*types.User{} + if err := unmarshal("testdata/users.json", &vv); err != nil { + return nil, err + } + for _, v := range vv { + v.Salt = fmt.Sprintf("%x", v.Email) + if err := store.Create(noContext, v); err != nil { + return nil, err + } + } + return store, nil +} diff --git a/internal/store/database/util.go b/internal/store/database/util.go new file mode 100644 index 000000000..195bf8cd8 --- /dev/null +++ b/internal/store/database/util.go @@ -0,0 +1,28 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +// default query range limit. +const defaultLimit = 100 + +// limit returns the page size to a sql limit. +func limit(size int) int { + if size == 0 { + size = defaultLimit + } + return size +} + +// offset converts the page to a sql offset. +func offset(page, size int) int { + if page == 0 { + page = 1 + } + if size == 0 { + size = defaultLimit + } + page = page - 1 + return page * size +} diff --git a/internal/store/database/util_test.go b/internal/store/database/util_test.go new file mode 100644 index 000000000..a938e2853 --- /dev/null +++ b/internal/store/database/util_test.go @@ -0,0 +1,76 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import "testing" + +func TestOffset(t *testing.T) { + tests := []struct { + page int + size int + want int + }{ + { + page: 0, + size: 10, + want: 0, + }, + { + page: 1, + size: 10, + want: 0, + }, + { + page: 2, + size: 10, + want: 10, + }, + { + page: 3, + size: 10, + want: 20, + }, + { + page: 4, + size: 100, + want: 300, + }, + { + page: 4, + size: 0, // unset, expect default 100 + want: 300, + }, + } + + for _, test := range tests { + got, want := offset(test.page, test.size), test.want + if got != want { + t.Errorf("Got %d want %d for page %d, size %d", got, want, test.page, test.size) + } + } +} + +func TestLimit(t *testing.T) { + tests := []struct { + size int + want int + }{ + { + size: 0, + want: 100, + }, + { + size: 10, + want: 10, + }, + } + + for _, test := range tests { + got, want := limit(test.size), test.want + if got != want { + t.Errorf("Got %d want %d for size %d", got, want, test.size) + } + } +} diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go new file mode 100644 index 000000000..4fc7ea8f5 --- /dev/null +++ b/internal/store/database/wire.go @@ -0,0 +1,65 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "github.com/bradrydzewski/my-app/internal/store" + "github.com/bradrydzewski/my-app/types" + + "github.com/google/wire" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package +var WireSet = wire.NewSet( + ProvideDatabase, + ProvideUserStore, + ProvidePipelineStore, + ProvideExecutionStore, +) + +// ProvideDatabase provides a database connection. +func ProvideDatabase(config *types.Config) (*sqlx.DB, error) { + return Connect( + config.Database.Driver, + config.Database.Datasource, + ) +} + +// ProvideUserStore provides a user store. +func ProvideUserStore(db *sqlx.DB) store.UserStore { + switch db.DriverName() { + case "postgres": + return NewUserStore(db) + default: + return NewUserStoreSync( + NewUserStore(db), + ) + } +} + +// ProvidePipelineStore provides a pipeline store. +func ProvidePipelineStore(db *sqlx.DB) store.PipelineStore { + switch db.DriverName() { + case "postgres": + return NewPipelineStore(db) + default: + return NewPipelineStoreSync( + NewPipelineStore(db), + ) + } +} + +// ProvideExecutionStore provides a execution store. +func ProvideExecutionStore(db *sqlx.DB) store.ExecutionStore { + switch db.DriverName() { + case "postgres": + return NewExecutionStore(db) + default: + return NewExecutionStoreSync( + NewExecutionStore(db), + ) + } +} diff --git a/internal/store/memory/config.go b/internal/store/memory/config.go new file mode 100644 index 000000000..34d0cfaa7 --- /dev/null +++ b/internal/store/memory/config.go @@ -0,0 +1,28 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package memory provides readonly memory data storage. +package memory + +import ( + "context" + + "github.com/bradrydzewski/my-app/types" +) + +// New returns a new system configuration store. +func New(config *types.Config) *SystemStore { + return &SystemStore{config: config} +} + +// SystemStore is a system store that loads system +// configuration parameters stored in the environment. +type SystemStore struct { + config *types.Config +} + +// Config returns the system configuration. +func (c *SystemStore) Config(ctx context.Context) *types.Config { + return c.config +} diff --git a/internal/store/memory/config_test.go b/internal/store/memory/config_test.go new file mode 100644 index 000000000..969e19c47 --- /dev/null +++ b/internal/store/memory/config_test.go @@ -0,0 +1,13 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package memory + +import "testing" + +// this unit test ensures the static configuration is returned +// from the in-memory system configuration store. +func TestConfig(t *testing.T) { + t.Skip() +} diff --git a/internal/store/memory/wire.go b/internal/store/memory/wire.go new file mode 100644 index 000000000..01061a9a7 --- /dev/null +++ b/internal/store/memory/wire.go @@ -0,0 +1,16 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package memory + +import ( + "github.com/bradrydzewski/my-app/internal/store" + "github.com/google/wire" +) + +// WireSet provides a wire set for this package +var WireSet = wire.NewSet( + New, + wire.Bind(new(store.SystemStore), new(*SystemStore)), +) diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 000000000..73b70d176 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,92 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package store defines the data storage interfaces. +package store + +import ( + "context" + + "github.com/bradrydzewski/my-app/types" +) + +type ( + // ExecutionStore defines execution data storage. + ExecutionStore interface { + // Find finds the execution by id. + Find(ctx context.Context, id int64) (*types.Execution, error) + + // FindSlug finds the execution by pipeline id and slug. + FindSlug(ctx context.Context, id int64, slug string) (*types.Execution, error) + + // List returns a list of executions by pipeline id. + List(ctx context.Context, id int64, params types.Params) ([]*types.Execution, error) + + // Create saves the execution details. + Create(ctx context.Context, execution *types.Execution) error + + // Update updates the execution details. + Update(ctx context.Context, execution *types.Execution) error + + // Delete deletes the execution. + Delete(ctx context.Context, execution *types.Execution) error + } + + // PipelineStore defines pipeline data storage. + PipelineStore interface { + // Find finds the pipeline by id. + Find(ctx context.Context, id int64) (*types.Pipeline, error) + + // FindToken finds the pipeline by token. + FindToken(ctx context.Context, token string) (*types.Pipeline, error) + + // FindSlug finds the user unique name. + FindSlug(ctx context.Context, key string) (*types.Pipeline, error) + + // List returns a list of pipelines by user. + List(ctx context.Context, user int64, params types.Params) ([]*types.Pipeline, error) + + // Create saves the pipeline details. + Create(ctx context.Context, pipeline *types.Pipeline) error + + // Update updates the pipeline details. + Update(ctx context.Context, pipeline *types.Pipeline) error + + // Delete deletes the pipeline. + Delete(ctx context.Context, pipeline *types.Pipeline) error + } + + // UserStore defines user data storage. + UserStore interface { + // Find finds the user by id. + Find(ctx context.Context, id int64) (*types.User, error) + + // FindEmail finds the user by email. + FindEmail(ctx context.Context, email string) (*types.User, error) + + // FindKey finds the user by unique key (email or id). + FindKey(ctx context.Context, key string) (*types.User, error) + + // List returns a list of users. + List(ctx context.Context, params types.UserFilter) ([]*types.User, error) + + // Create saves the user details. + Create(ctx context.Context, user *types.User) error + + // Update updates the user details. + Update(ctx context.Context, user *types.User) error + + // Delete deletes the user. + Delete(ctx context.Context, user *types.User) error + + // Count returns a count of users. + Count(ctx context.Context) (int64, error) + } + + // SystemStore defines insternal system metadata storage. + SystemStore interface { + // Config returns the system configuration. + Config(ctx context.Context) *types.Config + } +) diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 000000000..7fc5da5dc --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package store diff --git a/internal/testing/integration/integration.go b/internal/testing/integration/integration.go new file mode 100644 index 000000000..3bdf3be1e --- /dev/null +++ b/internal/testing/integration/integration.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package integration diff --git a/internal/testing/testing.go b/internal/testing/testing.go new file mode 100644 index 000000000..3db7f16ed --- /dev/null +++ b/internal/testing/testing.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package testing diff --git a/internal/token/token.go b/internal/token/token.go new file mode 100644 index 000000000..1dd5e57da --- /dev/null +++ b/internal/token/token.go @@ -0,0 +1,46 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package token + +import ( + "fmt" + "time" + + "github.com/bradrydzewski/my-app/types" + + "github.com/dgrijalva/jwt-go" +) + +// Claims defines custom token claims. +type Claims struct { + Admin bool `json:"admin"` + + jwt.StandardClaims +} + +// Generate generates a token with no expiration. +func Generate(user *types.User, secret string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ + user.Admin, + jwt.StandardClaims{ + Subject: fmt.Sprint(user.ID), + IssuedAt: time.Now().Unix(), + }, + }) + return token.SignedString([]byte(secret)) +} + +// GenerateExp generates a token with an expiration date. +func GenerateExp(user *types.User, exp int64, secret string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ + user.Admin, + jwt.StandardClaims{ + ExpiresAt: exp, + Subject: fmt.Sprint(user.ID), + IssuedAt: time.Now().Unix(), + }, + }) + return token.SignedString([]byte(secret)) +} diff --git a/internal/token/token_test.go b/internal/token/token_test.go new file mode 100644 index 000000000..e6e01c9c2 --- /dev/null +++ b/internal/token/token_test.go @@ -0,0 +1,107 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package token + +import ( + "strconv" + "testing" + "time" + + "github.com/bradrydzewski/my-app/types" + + "github.com/dgrijalva/jwt-go" +) + +func TestToken(t *testing.T) { + user := &types.User{ID: 42, Admin: true} + tokenStr, err := Generate(user, "TEST0E4C2F76C58916E") + if err != nil { + t.Error(err) + return + } + + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + sub := token.Claims.(*Claims).Subject + id, _ := strconv.ParseInt(sub, 10, 64) + if id != 42 { + t.Errorf("want subscriber id, got %v", id) + } + return []byte("TEST0E4C2F76C58916E"), nil + }) + if err != nil { + t.Error(err) + return + } + if token.Valid == false { + t.Errorf("invalid token") + return + } + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + t.Errorf("invalid token signing method") + return + } + + if expires := token.Claims.(*Claims).ExpiresAt; expires > 0 { + if time.Now().Unix() > expires { + t.Errorf("token expired") + } + } +} + +func TestTokenExpired(t *testing.T) { + user := &types.User{ID: 42, Admin: true} + tokenStr, err := GenerateExp(user, 1637549186, "TEST0E4C2F76C58916E") + if err != nil { + t.Error(err) + return + } + + _, err = jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + sub := token.Claims.(*Claims).Subject + id, _ := strconv.ParseInt(sub, 10, 64) + if id != 42 { + t.Errorf("want subscriber id, got %v", id) + } + return []byte("TEST0E4C2F76C58916E"), nil + }) + if err == nil { + t.Errorf("expect token expired") + return + } +} + +func TestTokenNotExpired(t *testing.T) { + user := &types.User{ID: 42, Admin: true} + tokenStr, err := GenerateExp(user, time.Now().Add(time.Hour).Unix(), "TEST0E4C2F76C58916E") + if err != nil { + t.Error(err) + return + } + + token_, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + sub := token.Claims.(*Claims).Subject + id, _ := strconv.ParseInt(sub, 10, 64) + if id != 42 { + t.Errorf("want subscriber id, got %v", id) + } + return []byte("TEST0E4C2F76C58916E"), nil + }) + if err != nil { + t.Error(err) + return + } + + if claims, ok := token_.Claims.(*Claims); ok { + if claims.ExpiresAt > 0 { + if time.Now().Unix() > claims.ExpiresAt { + t.Errorf("expect token not expired") + } + } else { + t.Errorf("expect token expiration greater than zero") + } + } else { + t.Errorf("expect token claims from token") + } +} diff --git a/main.go b/main.go new file mode 100644 index 000000000..1091d375a --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package main + +import ( + "github.com/bradrydzewski/my-app/cli" + + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + cli.Command() +} diff --git a/mocks/mock.go b/mocks/mock.go new file mode 100644 index 000000000..e3688d561 --- /dev/null +++ b/mocks/mock.go @@ -0,0 +1,9 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package mocks provides mock interfaces. +package mocks + +//go:generate mockgen -package=mocks -destination=mock_store.go github.com/bradrydzewski/my-app/internal/store ExecutionStore,PipelineStore,SystemStore,UserStore +//go:generate mockgen -package=mocks -destination=mock_client.go github.com/bradrydzewski/my-app/client Client diff --git a/mocks/mock_client.go b/mocks/mock_client.go new file mode 100644 index 000000000..1586c16c9 --- /dev/null +++ b/mocks/mock_client.go @@ -0,0 +1,317 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/bradrydzewski/my-app/client (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + types "github.com/bradrydzewski/my-app/types" + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Execution mocks base method. +func (m *MockClient) Execution(arg0, arg1 string) (*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Execution", arg0, arg1) + ret0, _ := ret[0].(*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Execution indicates an expected call of Execution. +func (mr *MockClientMockRecorder) Execution(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execution", reflect.TypeOf((*MockClient)(nil).Execution), arg0, arg1) +} + +// ExecutionCreate mocks base method. +func (m *MockClient) ExecutionCreate(arg0 string, arg1 *types.Execution) (*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecutionCreate", arg0, arg1) + ret0, _ := ret[0].(*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecutionCreate indicates an expected call of ExecutionCreate. +func (mr *MockClientMockRecorder) ExecutionCreate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionCreate", reflect.TypeOf((*MockClient)(nil).ExecutionCreate), arg0, arg1) +} + +// ExecutionDelete mocks base method. +func (m *MockClient) ExecutionDelete(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecutionDelete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExecutionDelete indicates an expected call of ExecutionDelete. +func (mr *MockClientMockRecorder) ExecutionDelete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionDelete", reflect.TypeOf((*MockClient)(nil).ExecutionDelete), arg0, arg1) +} + +// ExecutionList mocks base method. +func (m *MockClient) ExecutionList(arg0 string, arg1 types.Params) ([]*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecutionList", arg0, arg1) + ret0, _ := ret[0].([]*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecutionList indicates an expected call of ExecutionList. +func (mr *MockClientMockRecorder) ExecutionList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionList", reflect.TypeOf((*MockClient)(nil).ExecutionList), arg0, arg1) +} + +// ExecutionUpdate mocks base method. +func (m *MockClient) ExecutionUpdate(arg0, arg1 string, arg2 *types.ExecutionInput) (*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecutionUpdate", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecutionUpdate indicates an expected call of ExecutionUpdate. +func (mr *MockClientMockRecorder) ExecutionUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionUpdate", reflect.TypeOf((*MockClient)(nil).ExecutionUpdate), arg0, arg1, arg2) +} + +// Login mocks base method. +func (m *MockClient) Login(arg0, arg1 string) (*types.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", arg0, arg1) + ret0, _ := ret[0].(*types.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Login indicates an expected call of Login. +func (mr *MockClientMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockClient)(nil).Login), arg0, arg1) +} + +// Pipeline mocks base method. +func (m *MockClient) Pipeline(arg0 string) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Pipeline", arg0) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Pipeline indicates an expected call of Pipeline. +func (mr *MockClientMockRecorder) Pipeline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipeline", reflect.TypeOf((*MockClient)(nil).Pipeline), arg0) +} + +// PipelineCreate mocks base method. +func (m *MockClient) PipelineCreate(arg0 *types.Pipeline) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PipelineCreate", arg0) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PipelineCreate indicates an expected call of PipelineCreate. +func (mr *MockClientMockRecorder) PipelineCreate(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PipelineCreate", reflect.TypeOf((*MockClient)(nil).PipelineCreate), arg0) +} + +// PipelineDelete mocks base method. +func (m *MockClient) PipelineDelete(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PipelineDelete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// PipelineDelete indicates an expected call of PipelineDelete. +func (mr *MockClientMockRecorder) PipelineDelete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PipelineDelete", reflect.TypeOf((*MockClient)(nil).PipelineDelete), arg0) +} + +// PipelineList mocks base method. +func (m *MockClient) PipelineList(arg0 types.Params) ([]*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PipelineList", arg0) + ret0, _ := ret[0].([]*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PipelineList indicates an expected call of PipelineList. +func (mr *MockClientMockRecorder) PipelineList(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PipelineList", reflect.TypeOf((*MockClient)(nil).PipelineList), arg0) +} + +// PipelineUpdate mocks base method. +func (m *MockClient) PipelineUpdate(arg0 string, arg1 *types.PipelineInput) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PipelineUpdate", arg0, arg1) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PipelineUpdate indicates an expected call of PipelineUpdate. +func (mr *MockClientMockRecorder) PipelineUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PipelineUpdate", reflect.TypeOf((*MockClient)(nil).PipelineUpdate), arg0, arg1) +} + +// Register mocks base method. +func (m *MockClient) Register(arg0, arg1 string) (*types.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Register", arg0, arg1) + ret0, _ := ret[0].(*types.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Register indicates an expected call of Register. +func (mr *MockClientMockRecorder) Register(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1) +} + +// Self mocks base method. +func (m *MockClient) Self() (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Self") + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Self indicates an expected call of Self. +func (mr *MockClientMockRecorder) Self() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Self", reflect.TypeOf((*MockClient)(nil).Self)) +} + +// Token mocks base method. +func (m *MockClient) Token() (*types.Token, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Token") + ret0, _ := ret[0].(*types.Token) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Token indicates an expected call of Token. +func (mr *MockClientMockRecorder) Token() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Token", reflect.TypeOf((*MockClient)(nil).Token)) +} + +// User mocks base method. +func (m *MockClient) User(arg0 string) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "User", arg0) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// User indicates an expected call of User. +func (mr *MockClientMockRecorder) User(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "User", reflect.TypeOf((*MockClient)(nil).User), arg0) +} + +// UserCreate mocks base method. +func (m *MockClient) UserCreate(arg0 *types.User) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserCreate", arg0) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UserCreate indicates an expected call of UserCreate. +func (mr *MockClientMockRecorder) UserCreate(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCreate", reflect.TypeOf((*MockClient)(nil).UserCreate), arg0) +} + +// UserDelete mocks base method. +func (m *MockClient) UserDelete(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserDelete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UserDelete indicates an expected call of UserDelete. +func (mr *MockClientMockRecorder) UserDelete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDelete", reflect.TypeOf((*MockClient)(nil).UserDelete), arg0) +} + +// UserList mocks base method. +func (m *MockClient) UserList(arg0 types.Params) ([]*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserList", arg0) + ret0, _ := ret[0].([]*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UserList indicates an expected call of UserList. +func (mr *MockClientMockRecorder) UserList(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserList", reflect.TypeOf((*MockClient)(nil).UserList), arg0) +} + +// UserUpdate mocks base method. +func (m *MockClient) UserUpdate(arg0 string, arg1 *types.UserInput) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserUpdate", arg0, arg1) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UserUpdate indicates an expected call of UserUpdate. +func (mr *MockClientMockRecorder) UserUpdate(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserUpdate", reflect.TypeOf((*MockClient)(nil).UserUpdate), arg0, arg1) +} diff --git a/mocks/mock_store.go b/mocks/mock_store.go new file mode 100644 index 000000000..0706190a0 --- /dev/null +++ b/mocks/mock_store.go @@ -0,0 +1,425 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/bradrydzewski/my-app/internal/store (interfaces: ExecutionStore,PipelineStore,SystemStore,UserStore) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + types "github.com/bradrydzewski/my-app/types" + gomock "github.com/golang/mock/gomock" +) + +// MockExecutionStore is a mock of ExecutionStore interface. +type MockExecutionStore struct { + ctrl *gomock.Controller + recorder *MockExecutionStoreMockRecorder +} + +// MockExecutionStoreMockRecorder is the mock recorder for MockExecutionStore. +type MockExecutionStoreMockRecorder struct { + mock *MockExecutionStore +} + +// NewMockExecutionStore creates a new mock instance. +func NewMockExecutionStore(ctrl *gomock.Controller) *MockExecutionStore { + mock := &MockExecutionStore{ctrl: ctrl} + mock.recorder = &MockExecutionStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExecutionStore) EXPECT() *MockExecutionStoreMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockExecutionStore) Create(arg0 context.Context, arg1 *types.Execution) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockExecutionStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockExecutionStore)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockExecutionStore) Delete(arg0 context.Context, arg1 *types.Execution) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockExecutionStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockExecutionStore)(nil).Delete), arg0, arg1) +} + +// Find mocks base method. +func (m *MockExecutionStore) Find(arg0 context.Context, arg1 int64) (*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1) + ret0, _ := ret[0].(*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockExecutionStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockExecutionStore)(nil).Find), arg0, arg1) +} + +// FindSlug mocks base method. +func (m *MockExecutionStore) FindSlug(arg0 context.Context, arg1 int64, arg2 string) (*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindSlug", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindSlug indicates an expected call of FindSlug. +func (mr *MockExecutionStoreMockRecorder) FindSlug(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSlug", reflect.TypeOf((*MockExecutionStore)(nil).FindSlug), arg0, arg1, arg2) +} + +// List mocks base method. +func (m *MockExecutionStore) List(arg0 context.Context, arg1 int64, arg2 types.Params) ([]*types.Execution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) + ret0, _ := ret[0].([]*types.Execution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockExecutionStoreMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockExecutionStore)(nil).List), arg0, arg1, arg2) +} + +// Update mocks base method. +func (m *MockExecutionStore) Update(arg0 context.Context, arg1 *types.Execution) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockExecutionStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockExecutionStore)(nil).Update), arg0, arg1) +} + +// MockPipelineStore is a mock of PipelineStore interface. +type MockPipelineStore struct { + ctrl *gomock.Controller + recorder *MockPipelineStoreMockRecorder +} + +// MockPipelineStoreMockRecorder is the mock recorder for MockPipelineStore. +type MockPipelineStoreMockRecorder struct { + mock *MockPipelineStore +} + +// NewMockPipelineStore creates a new mock instance. +func NewMockPipelineStore(ctrl *gomock.Controller) *MockPipelineStore { + mock := &MockPipelineStore{ctrl: ctrl} + mock.recorder = &MockPipelineStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPipelineStore) EXPECT() *MockPipelineStoreMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockPipelineStore) Create(arg0 context.Context, arg1 *types.Pipeline) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockPipelineStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPipelineStore)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockPipelineStore) Delete(arg0 context.Context, arg1 *types.Pipeline) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockPipelineStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPipelineStore)(nil).Delete), arg0, arg1) +} + +// Find mocks base method. +func (m *MockPipelineStore) Find(arg0 context.Context, arg1 int64) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockPipelineStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockPipelineStore)(nil).Find), arg0, arg1) +} + +// FindSlug mocks base method. +func (m *MockPipelineStore) FindSlug(arg0 context.Context, arg1 string) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindSlug", arg0, arg1) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindSlug indicates an expected call of FindSlug. +func (mr *MockPipelineStoreMockRecorder) FindSlug(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSlug", reflect.TypeOf((*MockPipelineStore)(nil).FindSlug), arg0, arg1) +} + +// FindToken mocks base method. +func (m *MockPipelineStore) FindToken(arg0 context.Context, arg1 string) (*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindToken", arg0, arg1) + ret0, _ := ret[0].(*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindToken indicates an expected call of FindToken. +func (mr *MockPipelineStoreMockRecorder) FindToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindToken", reflect.TypeOf((*MockPipelineStore)(nil).FindToken), arg0, arg1) +} + +// List mocks base method. +func (m *MockPipelineStore) List(arg0 context.Context, arg1 int64, arg2 types.Params) ([]*types.Pipeline, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1, arg2) + ret0, _ := ret[0].([]*types.Pipeline) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockPipelineStoreMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPipelineStore)(nil).List), arg0, arg1, arg2) +} + +// Update mocks base method. +func (m *MockPipelineStore) Update(arg0 context.Context, arg1 *types.Pipeline) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockPipelineStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPipelineStore)(nil).Update), arg0, arg1) +} + +// MockSystemStore is a mock of SystemStore interface. +type MockSystemStore struct { + ctrl *gomock.Controller + recorder *MockSystemStoreMockRecorder +} + +// MockSystemStoreMockRecorder is the mock recorder for MockSystemStore. +type MockSystemStoreMockRecorder struct { + mock *MockSystemStore +} + +// NewMockSystemStore creates a new mock instance. +func NewMockSystemStore(ctrl *gomock.Controller) *MockSystemStore { + mock := &MockSystemStore{ctrl: ctrl} + mock.recorder = &MockSystemStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSystemStore) EXPECT() *MockSystemStoreMockRecorder { + return m.recorder +} + +// Config mocks base method. +func (m *MockSystemStore) Config(arg0 context.Context) *types.Config { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Config", arg0) + ret0, _ := ret[0].(*types.Config) + return ret0 +} + +// Config indicates an expected call of Config. +func (mr *MockSystemStoreMockRecorder) Config(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockSystemStore)(nil).Config), arg0) +} + +// MockUserStore is a mock of UserStore interface. +type MockUserStore struct { + ctrl *gomock.Controller + recorder *MockUserStoreMockRecorder +} + +// MockUserStoreMockRecorder is the mock recorder for MockUserStore. +type MockUserStoreMockRecorder struct { + mock *MockUserStore +} + +// NewMockUserStore creates a new mock instance. +func NewMockUserStore(ctrl *gomock.Controller) *MockUserStore { + mock := &MockUserStore{ctrl: ctrl} + mock.recorder = &MockUserStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUserStore) EXPECT() *MockUserStoreMockRecorder { + return m.recorder +} + +// Count mocks base method. +func (m *MockUserStore) Count(arg0 context.Context) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count", arg0) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockUserStoreMockRecorder) Count(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockUserStore)(nil).Count), arg0) +} + +// Create mocks base method. +func (m *MockUserStore) Create(arg0 context.Context, arg1 *types.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockUserStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUserStore)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockUserStore) Delete(arg0 context.Context, arg1 *types.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockUserStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserStore)(nil).Delete), arg0, arg1) +} + +// Find mocks base method. +func (m *MockUserStore) Find(arg0 context.Context, arg1 int64) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Find", arg0, arg1) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Find indicates an expected call of Find. +func (mr *MockUserStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockUserStore)(nil).Find), arg0, arg1) +} + +// FindEmail mocks base method. +func (m *MockUserStore) FindEmail(arg0 context.Context, arg1 string) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindEmail", arg0, arg1) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindEmail indicates an expected call of FindEmail. +func (mr *MockUserStoreMockRecorder) FindEmail(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEmail", reflect.TypeOf((*MockUserStore)(nil).FindEmail), arg0, arg1) +} + +// FindKey mocks base method. +func (m *MockUserStore) FindKey(arg0 context.Context, arg1 string) (*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindKey", arg0, arg1) + ret0, _ := ret[0].(*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindKey indicates an expected call of FindKey. +func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindKey", reflect.TypeOf((*MockUserStore)(nil).FindKey), arg0, arg1) +} + +// List mocks base method. +func (m *MockUserStore) List(arg0 context.Context, arg1 types.UserFilter) ([]*types.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].([]*types.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockUserStoreMockRecorder) List(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUserStore)(nil).List), arg0, arg1) +} + +// Update mocks base method. +func (m *MockUserStore) Update(arg0 context.Context, arg1 *types.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockUserStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserStore)(nil).Update), arg0, arg1) +} diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 000000000..b9041f462 --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,15 @@ +{ + "systemParams": "darwin-x64-93", + "modulesFolders": [], + "flags": [], + "linkedModules": [ + "@harnessio/ff-javascript-client-sdk", + "@harnessio/mentions", + "@wings-software/language-server", + "monaco-yaml" + ], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/types/check/execution.go b/types/check/execution.go new file mode 100644 index 000000000..fde2f4a48 --- /dev/null +++ b/types/check/execution.go @@ -0,0 +1,48 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "errors" + + "github.com/bradrydzewski/my-app/types" + + "github.com/gosimple/slug" +) + +var ( + // ErrExecutionIdentifier is returned when the execution + // slug is an invalid format. + ErrExecutionIdentifier = errors.New("Invalid execution identifier") + + // ErrExecutionIdentifierLen is returned when the execution + // name exceeds the maximum number of characters. + ErrExecutionIdentifierLen = errors.New("Execution identifier cannot exceed 250 characters") + + // ErrExecutionNameLen is returned when the execution name + // exceeds the maximum number of characters. + ErrExecutionNameLen = errors.New("Execution name cannot exceed 250 characters") + + // ErrExecutionDescLen is returned when the execution desc + // exceeds the maximum number of characters. + ErrExecutionDescLen = errors.New("Execution description cannot exceed 250 characters") +) + +// Execution returns true if the Execution if valid. +func Execution(execution *types.Execution) (bool, error) { + if !slug.IsSlug(execution.Slug) { + return false, ErrExecutionIdentifier + } + if len(execution.Slug) > 250 { + return false, ErrExecutionIdentifierLen + } + if len(execution.Name) > 250 { + return false, ErrExecutionNameLen + } + if len(execution.Desc) > 500 { + return false, ErrExecutionDescLen + } + return true, nil +} diff --git a/types/check/execution_test.go b/types/check/execution_test.go new file mode 100644 index 000000000..9012b6c40 --- /dev/null +++ b/types/check/execution_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check diff --git a/types/check/pipeline.go b/types/check/pipeline.go new file mode 100644 index 000000000..bda312305 --- /dev/null +++ b/types/check/pipeline.go @@ -0,0 +1,48 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "errors" + + "github.com/bradrydzewski/my-app/types" + + "github.com/gosimple/slug" +) + +var ( + // ErrPipelineIdentifier is returned when the pipeline + // slug is an invalid format. + ErrPipelineIdentifier = errors.New("Invalid pipeline identifier") + + // ErrPipelineIdentifierLen is returned when the pipeline + // name exceeds the maximum number of characters. + ErrPipelineIdentifierLen = errors.New("Pipeline identifier cannot exceed 250 characters") + + // ErrPipelineNameLen is returned when the pipeline name + // exceeds the maximum number of characters. + ErrPipelineNameLen = errors.New("Pipeline name cannot exceed 250 characters") + + // ErrPipelineDescLen is returned when the pipeline desc + // exceeds the maximum number of characters. + ErrPipelineDescLen = errors.New("Pipeline description cannot exceed 250 characters") +) + +// Pipeline returns true if the Pipeline if valid. +func Pipeline(pipeline *types.Pipeline) (bool, error) { + if !slug.IsSlug(pipeline.Slug) { + return false, ErrPipelineIdentifier + } + if len(pipeline.Slug) > 250 { + return false, ErrPipelineIdentifierLen + } + if len(pipeline.Name) > 250 { + return false, ErrPipelineNameLen + } + if len(pipeline.Desc) > 500 { + return false, ErrPipelineDescLen + } + return true, nil +} diff --git a/types/check/user.go b/types/check/user.go new file mode 100644 index 000000000..cd1a949b3 --- /dev/null +++ b/types/check/user.go @@ -0,0 +1,25 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "errors" + + "github.com/bradrydzewski/my-app/types" +) + +var ( + // ErrEmailLen is returned when the email address + // exceeds the maximum number of characters. + ErrEmailLen = errors.New("Email address cannot exceed 250 characters") +) + +// User returns true if the User if valid. +func User(user *types.User) (bool, error) { + if len(user.Email) > 250 { + return false, ErrEmailLen + } + return true, nil +} diff --git a/types/check/user_test.go b/types/check/user_test.go new file mode 100644 index 000000000..63b892c1a --- /dev/null +++ b/types/check/user_test.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package check + +import ( + "testing" + + "github.com/bradrydzewski/my-app/types" +) + +func TestUser(t *testing.T) { + tests := []struct { + email string + error error + valid bool + }{ + { + email: "jane@gmail.com", + valid: true, + }, + } + for _, test := range tests { + user := &types.User{Email: test.email} + ok, err := User(user) + if got, want := ok, test.valid; got != want { + t.Errorf("Want user %s is valid %v, got %v", test.email, want, got) + } + if got, want := err, test.error; got != want { + t.Errorf("Want user %s error %v, got %v", test.email, want, got) + } + } +} diff --git a/types/config.go b/types/config.go new file mode 100644 index 000000000..155bebf3e --- /dev/null +++ b/types/config.go @@ -0,0 +1,67 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types + +import "time" + +// Config stores the system configuration. +type Config struct { + Debug bool `envconfig:"APP_DEBUG"` + Trace bool `envconfig:"APP_TRACE"` + + // Server defines the server configuration parameters. + Server struct { + Bind string `envconfig:"APP_HTTP_BIND" default:":3000"` + Proto string `envconfig:"APP_HTTP_PROTO"` + Host string `envconfig:"APP_HTTP_HOST"` + + // Acme defines Acme configuration parameters. + Acme struct { + Enabled bool `envconfig:"APP_ACME_ENABLED"` + Endpont string `envconfig:"APP_ACME_ENDPOINT"` + Email bool `envconfig:"APP_ACME_EMAIL"` + } + } + + // Database defines the database configuration parameters. + Database struct { + Driver string `envconfig:"APP_DATABASE_DRIVER" default:"sqlite3"` + Datasource string `envconfig:"APP_DATABASE_DATASOURCE" default:"database.sqlite3"` + } + + // Token defines token configuration parameters. + Token struct { + Expire time.Duration `envconfig:"APP_TOKEN_EXPIRE" default:"720h"` + } + + // Cors defines http cors parameters + Cors struct { + AllowedOrigins []string `envconfig:"APP_CORS_ALLOWED_ORIGINS" default:"*"` + AllowedMethods []string `envconfig:"APP_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"` + AllowedHeaders []string `envconfig:"APP_CORS_ALLOWED_HEADERS" default:"Origin,Accept,Accept-Language,Authorization,Content-Type,Content-Language,X-Requested-With,X-Request-Id"` + ExposedHeaders []string `envconfig:"APP_CORS_EXPOSED_HEADERS" default:"Link"` + AllowCredentials bool `envconfig:"APP_CORS_ALLOW_CREDENTIALS" default:"true"` + MaxAge int `envconfig:"APP_CORS_MAX_AGE" default:"300"` + } + + // Secure defines http security parameters. + Secure struct { + AllowedHosts []string `envconfig:"APP_HTTP_ALLOWED_HOSTS"` + HostsProxyHeaders []string `envconfig:"APP_HTTP_PROXY_HEADERS"` + SSLRedirect bool `envconfig:"APP_HTTP_SSL_REDIRECT"` + SSLTemporaryRedirect bool `envconfig:"APP_HTTP_SSL_TEMPORARY_REDIRECT"` + SSLHost string `envconfig:"APP_HTTP_SSL_HOST"` + SSLProxyHeaders map[string]string `envconfig:"APP_HTTP_SSL_PROXY_HEADERS"` + STSSeconds int64 `envconfig:"APP_HTTP_STS_SECONDS"` + STSIncludeSubdomains bool `envconfig:"APP_HTTP_STS_INCLUDE_SUBDOMAINS"` + STSPreload bool `envconfig:"APP_HTTP_STS_PRELOAD"` + ForceSTSHeader bool `envconfig:"APP_HTTP_STS_FORCE_HEADER"` + BrowserXSSFilter bool `envconfig:"APP_HTTP_BROWSER_XSS_FILTER" default:"true"` + FrameDeny bool `envconfig:"APP_HTTP_FRAME_DENY" default:"true"` + ContentTypeNosniff bool `envconfig:"APP_HTTP_CONTENT_TYPE_NO_SNIFF"` + ContentSecurityPolicy string `envconfig:"APP_HTTP_CONTENT_SECURITY_POLICY"` + ReferrerPolicy string `envconfig:"APP_HTTP_REFERRER_POLICY"` + } +} diff --git a/types/config_test.go b/types/config_test.go new file mode 100644 index 000000000..679f1390b --- /dev/null +++ b/types/config_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types diff --git a/types/enum/enum.go b/types/enum/enum.go new file mode 100644 index 000000000..cc002286d --- /dev/null +++ b/types/enum/enum.go @@ -0,0 +1,6 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package enum defines common enumerations. +package enum diff --git a/types/enum/order.go b/types/enum/order.go new file mode 100644 index 000000000..56f7e9ab9 --- /dev/null +++ b/types/enum/order.go @@ -0,0 +1,42 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "strings" + +// Order defines the sorder order. +type Order int + +// Order enumeration. +const ( + OrderDefault Order = iota + OrderAsc + OrderDesc +) + +// String returns the Order as a string. +func (e Order) String() (s string) { + switch e { + case OrderAsc: + return "asc" + case OrderDesc: + return "desc" + default: + return "asc" // ascending by default? + } +} + +// ParseOrder parses the order string and returns +// an order enumeration. +func ParseOrder(s string) Order { + switch strings.ToLower(s) { + case "asc", "ascending": + return OrderAsc + case "desc", "descending": + return OrderDesc + default: + return OrderDefault + } +} diff --git a/types/enum/order_test.go b/types/enum/order_test.go new file mode 100644 index 000000000..e1ab53dc6 --- /dev/null +++ b/types/enum/order_test.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "testing" + +func TestParseOrder(t *testing.T) { + tests := []struct { + text string + want Order + }{ + {"asc", OrderAsc}, + {"Asc", OrderAsc}, + {"ASC", OrderAsc}, + {"ascending", OrderAsc}, + {"Ascending", OrderAsc}, + {"desc", OrderDesc}, + {"Desc", OrderDesc}, + {"DESC", OrderDesc}, + {"descending", OrderDesc}, + {"Descending", OrderDesc}, + {"", OrderDefault}, + {"invalid", OrderDefault}, + } + + for _, test := range tests { + got, want := ParseOrder(test.text), test.want + if got != want { + t.Errorf("Want order %q parsed as %q, got %q", test.text, want, got) + } + } +} diff --git a/types/enum/role.go b/types/enum/role.go new file mode 100644 index 000000000..62a87a3c4 --- /dev/null +++ b/types/enum/role.go @@ -0,0 +1,48 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "encoding/json" + +// Role defines the member role. +type Role int + +// Role enumeration. +const ( + RoleDeveloper Role = iota + RoleAdmin +) + +// String returns the Role as a string. +func (e Role) String() string { + switch e { + case RoleDeveloper: + return "developer" + case RoleAdmin: + return "admin" + default: + return "developer" + } +} + +// MarshalJSON marshals the Type as a JSON string. +func (e Role) MarshalJSON() ([]byte, error) { + return json.Marshal(e.String()) +} + +// UnmarshalJSON unmashals a quoted json string to the enum value. +func (e *Role) UnmarshalJSON(b []byte) error { + var v string + json.Unmarshal(b, &v) + switch v { + case "admin": + *e = RoleAdmin + case "developer": + *e = RoleDeveloper + default: + *e = RoleDeveloper + } + return nil +} diff --git a/types/enum/role_test.go b/types/enum/role_test.go new file mode 100644 index 000000000..62d8241a8 --- /dev/null +++ b/types/enum/role_test.go @@ -0,0 +1,19 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "testing" + +func TestRoleUnmarshal(t *testing.T) { + t.Skip() +} + +func TestRoleMarshal(t *testing.T) { + t.Skip() +} + +func TestRoleString(t *testing.T) { + t.Skip() +} diff --git a/types/enum/user.go b/types/enum/user.go new file mode 100644 index 000000000..9ef06c256 --- /dev/null +++ b/types/enum/user.go @@ -0,0 +1,43 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "strings" + +// UserField defines user attributes that can be +// used for sorting and filtering. +type UserAttr int + +// Order enumeration. +const ( + UserAttrNone UserAttr = iota + UserAttrId + UserAttrName + UserAttrEmail + UserAttrAdmin + UserAttrCreated + UserAttrUpdated +) + +// ParseUserAttr parses the user attribute string +// and returns the equivalent enumeration. +func ParseUserAttr(s string) UserAttr { + switch strings.ToLower(s) { + case "id": + return UserAttrId + case "name": + return UserAttrName + case "email": + return UserAttrEmail + case "admin": + return UserAttrAdmin + case "created", "created_at": + return UserAttrCreated + case "updated", "updated_at": + return UserAttrUpdated + default: + return UserAttrNone + } +} diff --git a/types/enum/user_test.go b/types/enum/user_test.go new file mode 100644 index 000000000..d4dad3835 --- /dev/null +++ b/types/enum/user_test.go @@ -0,0 +1,30 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +import "testing" + +func TestParseUserAttr(t *testing.T) { + tests := []struct { + text string + want UserAttr + }{ + {"id", UserAttrId}, + {"name", UserAttrName}, + {"email", UserAttrEmail}, + {"created", UserAttrCreated}, + {"updated", UserAttrUpdated}, + {"admin", UserAttrAdmin}, + {"", UserAttrNone}, + {"invalid", UserAttrNone}, + } + + for _, test := range tests { + got, want := ParseUserAttr(test.text), test.want + if got != want { + t.Errorf("Want user attribute %q parsed as %q, got %q", test.text, want, got) + } + } +} diff --git a/types/types.go b/types/types.go new file mode 100644 index 000000000..f1a3dca0a --- /dev/null +++ b/types/types.go @@ -0,0 +1,170 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package types defines common data structures. +package types + +import ( + "time" + + "github.com/bradrydzewski/my-app/types/enum" +) + +type ( + // Scope defines the data scope. + Scope struct { + Account string + Organization string + Project string + Redirect string + } + + // Params stores query parameters. + Params struct { + Page int `json:"page"` + Size int `json:"size"` + Sort string `json:"sort"` + Order enum.Order `json:"direction"` + } + + // Execution stores execution details. + Execution struct { + ID int64 `db:"execution_id" json:"id"` + Pipeline int64 `db:"execution_pipeline_id" json:"pipeline,omitempty"` + Slug string `db:"execution_slug" json:"slug"` + Name string `db:"execution_name" json:"name"` + Desc string `db:"execution_desc" json:"desc"` + Created int64 `db:"execution_created" json:"created"` + Updated int64 `db:"execution_updated" json:"updated"` + } + + // ExecutionParams stores execution parameters. + ExecutionParams struct { + Pipeline int64 + Slug string + + Scope Scope + } + + // ExecutionListParams stores execution list + // parameters. + ExecutionListParams struct { + Pipeline int64 + + Query Params + Scope Scope + } + + // ExecutionInput store details used to create or + // update a execution. + ExecutionInput struct { + Slug *string `json:"slug"` + Name *string `json:"name"` + Desc *string `json:"desc"` + } + + // Pipeline stores pipeline details. + Pipeline struct { + ID int64 `db:"pipeline_id" json:"id"` + Name string `db:"pipeline_name" json:"name"` + Slug string `db:"pipeline_slug" json:"slug"` + Desc string `db:"pipeline_desc" json:"desc"` + Token string `db:"pipeline_token" json:"-"` + Active bool `db:"pipeline_active" json:"active"` + Created int64 `db:"pipeline_created" json:"created"` + Updated int64 `db:"pipeline_updated" json:"updated"` + } + + // PipelineParams stores pipeline parameters. + PipelineParams struct { + Slug string + + Scope Scope + } + + // PipelineListParams stores pipeline list + // parameters. + PipelineListParams struct { + Query Params + Scope Scope + } + + // PipelineInput store user pipeline details used to + // create or update a pipeline. + PipelineInput struct { + Slug *string `json:"slug"` + Name *string `json:"name"` + Desc *string `json:"desc"` + } + + // User stores user account details. + User struct { + ID int64 `db:"user_id" json:"id"` + Email string `db:"user_email" json:"email"` + Password string `db:"user_password" json:"-"` + Salt string `db:"user_salt" json:"-"` + Name string `db:"user_name" json:"name"` + Company string `db:"user_company" json:"company"` + Admin bool `db:"user_admin" json:"admin"` + Blocked bool `db:"user_blocked" json:"-"` + Created int64 `db:"user_created" json:"created"` + Updated int64 `db:"user_updated" json:"updated"` + Authed int64 `db:"user_authed" json:"authed"` + } + + // UserInput store user account details used to + // create or update a user. + UserInput struct { + Username *string `json:"email"` + Password *string `json:"password"` + Name *string `json:"name"` + Company *string `json:"company"` + Admin *bool `json:"admin"` + } + + // UserFilter stores user query parameters. + UserFilter struct { + Page int `json:"page"` + Size int `json:"size"` + Sort enum.UserAttr `json:"sort"` + Order enum.Order `json:"direction"` + } + + // Token stores token details. + Token struct { + Value string `json:"access_token"` + Address string `json:"uri,omitempty"` + Expires time.Time `json:"expires_at,omitempty"` + } + + // UserToken stores user account and token details. + UserToken struct { + User *User `json:"user"` + Token *Token `json:"token"` + } + + // Project stores project details. + Project struct { + Identifier string `json:"identifier"` + Color string `json:"color"` + Desc string `json:"description"` + Name string `json:"name"` + Modules []string `json:"modules"` + Org string `json:"orgIdentifier"` + Tags map[string]string `json:"tags"` + } + + // ProjectList stores the project list and project + // result set metdata. + ProjectList struct { + Data []*Project `json:"data"` + + Empty bool `json:"empty"` + PageIndex int `json:"pageIndex,omitempty"` + PageItemCount int `json:"pageItemCount,omitempty"` + PageSize int `json:"pageSize,omitempty"` + TotalItems int `json:"totalItems,omitempty"` + TotalPages int `json:"totalPages,omitempty"` + } +) diff --git a/types/types_test.go b/types/types_test.go new file mode 100644 index 000000000..679f1390b --- /dev/null +++ b/types/types_test.go @@ -0,0 +1,5 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types diff --git a/version/version.go b/version/version.go new file mode 100644 index 000000000..8f3656bb6 --- /dev/null +++ b/version/version.go @@ -0,0 +1,34 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +// Package version provides the version number. +package version + +import "github.com/coreos/go-semver/semver" + +var ( + // GitRepository is the git repository that was compiled + GitRepository string + // GitCommit is the git commit that was compiled + GitCommit string + // VersionMajor is for an API incompatible changes. + VersionMajor int64 = 1 + // VersionMinor is for functionality in a backwards-compatible manner. + VersionMinor int64 + // VersionPatch is for backwards-compatible bug fixes. + VersionPatch int64 + // VersionPre indicates prerelease. + VersionPre = "" + // VersionDev indicates development branch. Releases will be empty string. + VersionDev string +) + +// Version is the specification version that the package types support. +var Version = semver.Version{ + Major: VersionMajor, + Minor: VersionMinor, + Patch: VersionPatch, + PreRelease: semver.PreRelease(VersionPre), + Metadata: VersionDev, +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 000000000..cdc062615 --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,13 @@ +// Copyright 2021 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package version + +import "testing" + +func TestVersion(t *testing.T) { + if got, want := Version.String(), "1.0.0"; got != want { + t.Errorf("Want version %s, got %s", want, got) + } +} diff --git a/web/.eslintignore b/web/.eslintignore new file mode 100644 index 000000000..eb5fcac6b --- /dev/null +++ b/web/.eslintignore @@ -0,0 +1,6 @@ +src/services +jest.config.js +scripts/ +node_modules +dist +*.js \ No newline at end of file diff --git a/web/.eslintrc.yml b/web/.eslintrc.yml new file mode 100644 index 000000000..c36b3101d --- /dev/null +++ b/web/.eslintrc.yml @@ -0,0 +1,126 @@ +--- +parser: '@typescript-eslint/parser' +parserOptions: + ecmaVersion: 2020 + sourceType: module + ecmaFeatures: + jsx: true + impliedStrict: true + project: ./tsconfig-eslint.json +plugins: + - react + - '@typescript-eslint/eslint-plugin' + - react-hooks + - jest + - import +env: + browser: true + node: true + shared-node-browser: true + es6: true + jest: true +globals: + __DEV__: readonly +extends: + - eslint:recommended + - plugin:react/recommended + - plugin:@typescript-eslint/recommended + - plugin:import/errors + - plugin:import/typescript + - prettier +settings: + react: + version: detect + import/resolver: + typescript: + alwaysTryTypes: true +rules: + # custom rules + no-document-body-snapshot: 2 + duplicate-data-tooltip-id: 'warn' + jest-no-mock: + - 2 + - module: + react-router-dom: 'react-router-dom should not be mocked. Wrap the component inside TestWrapper instead' + + # built-in + no-console: 2 + semi: 0 + no-await-in-loop: 2 + no-shadow: 0 + + # react hooks + react-hooks/rules-of-hooks: 2 + react-hooks/exhaustive-deps: 1 + + # react + react/prop-types: 0 + react/display-name: 1 + + #typescript + '@typescript-eslint/no-use-before-define': 0 + '@typescript-eslint/explicit-function-return-type': + - 1 + - allowExpressions: true + no-unused-vars: 0 + '@typescript-eslint/no-unused-vars': + - 2 + - vars: all + args: after-used + ignoreRestSiblings: true + argsIgnorePattern: ^_ + '@typescript-eslint/member-delimiter-style': 0 + '@typescript-eslint/no-shadow': 2 + '@typescript-eslint/no-extra-semi': 0 + '@typescript-eslint/explicit-module-boundary-types': 0 + + #import + import/order: + - error + - groups: + - builtin + - external + - internal + - - parent + - sibling + pathGroups: + - pattern: '*.scss' + group: index + position: after + patternOptions: + matchBase: true + import/no-useless-path-segments: 2 + + no-restricted-imports: + - error + - patterns: + - lodash.* + paths: + - lodash + - name: yaml + importNames: + - stringify + message: 'Please use yamlStringify from @common/utils/YamlHelperMethods instead of this' + +overrides: + - files: + - '**/*.test.ts' + - '**/*.test.tsx' + rules: + '@typescript-eslint/no-magic-numbers': 0 + '@typescript-eslint/no-non-null-assertion': 0 + '@typescript-eslint/no-non-null-asserted-optional-chain': 0 + '@typescript-eslint/no-explicit-any': 0 + no-await-in-loop: 0 + jest/consistent-test-it: + - 2 + - fn: test + withinDescribe: test + jest/expect-expect: 2 + jest/no-disabled-tests: 2 + jest/no-commented-out-tests: 2 + - files: + - services.tsx + rules: + '@typescript-eslint/explicit-function-return-type': 0 + '@typescript-eslint/no-explicit-any': 0 diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 000000000..aea676d7e --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +coverage +.env +yarn-error* \ No newline at end of file diff --git a/web/.prettierrc.yml b/web/.prettierrc.yml new file mode 100644 index 000000000..065611bee --- /dev/null +++ b/web/.prettierrc.yml @@ -0,0 +1,10 @@ +--- +printWidth: 120 +tabWidth: 2 +useTabs: false +semi: false +singleQuote: true +trailingComma: none +bracketSpacing: true +bracketSameLine: true +arrowParens: avoid diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 000000000..d7df89c9c --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] +} diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json new file mode 100644 index 000000000..59a141f60 --- /dev/null +++ b/web/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "search.exclude": { + "**/node_modules": true, + "npm-debug.log*": true, + "**/static": true, + "dist/": true, + "yarn-error.*": true, + "**/yarn.lock": true + }, + "editor.formatOnSave": true, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "eslint.options": { + "rulePaths": ["./scripts/eslint-rules"] + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..26cf96d04 --- /dev/null +++ b/web/README.md @@ -0,0 +1,46 @@ +# Sample Module UI + +## Prerequisites + +``` +yarn setup-github-registry +``` + +## Local development + +Change current directory to policy-mgmt project folder and run API server: + +``` +APP_ENABLE_UI=false APP_ENABLE_STANDALONE=true APP_TOKEN_JWT_SECRET=1234 APP_INTERNAL_TOKEN_JWT_SECRET=5678 APP_HTTP_BIND=localhost:3001 go run main.go server +``` + +### Run the UI as a standalone app + +``` +yarn +yarn dev +``` + +Wait until Webpack build is done, then access http://localhost:3002/#/signin. + +Note that you can point standalone UI app to a non-local backend service by creating a `.env` (under `web` or project folder) with content looks like: + +``` +TARGET_LOCALHOST=false +BASE_URL=https://qa.harness.io/gateway +``` + +### Run the UI as a micro-frontend service + +Due to an issue with Webpack (reason still unknown), you can't mount micro-frontend app inside NextGen UI when it's being run under Webpack development mode (aka `yarn dev`). To overcome the issue, run: + +``` +yarn +yarn micro:watch +``` + +The micro front-end UI will be served under http://localhost:3000. Run [Core UI](https://github.com/harness/harness-core-ui/) locally and navigate to the app within NextGen UI. + +## Build + +UI build is integrated a a part of the backend build. See `.drone.yml` and `Taskfile.yml` for more information. diff --git a/web/dist.go b/web/dist.go new file mode 100644 index 000000000..989a72c99 --- /dev/null +++ b/web/dist.go @@ -0,0 +1,49 @@ +// Copyright 2021 Harness, Inc. All rights reserved. +// Use of this source code is governed by the Polyform License +// that can be found in the LICENSE.md file. + +//go:build !proxy +// +build !proxy + +// Package dist embeds the static web server content. +package web + +import ( + "embed" + "io/fs" + "net/http" + "path/filepath" +) + +//go:embed dist/* +var content embed.FS + +// Handler returns an http.HandlerFunc that servers the +// static content from the embedded file system. +func Handler() http.HandlerFunc { + // Load the files subdirectory + fs, err := fs.Sub(content, "dist") + if err != nil { + panic(err) + } + // Create an http.FileServer to serve the + // contents of the files subdiretory. + handler := http.FileServer(http.FS(fs)) + + // Create an http.HandlerFunc that wraps the + // http.FileServer to always load the index.html + // file if a directory path is being requested. + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // because this is a single page application, + // we need to always load the index.html file + // in the root of the project, unless the path + // points to a file with an extension (css, js, etc) + if filepath.Ext(r.URL.Path) == "" { + // HACK: alter the path to point to the + // root of the project. + r.URL.Path = "/" + } + // and finally server the file. + handler.ServeHTTP(w, r) + }) +} diff --git a/web/jest.config.js b/web/jest.config.js new file mode 100644 index 000000000..1a825abcf --- /dev/null +++ b/web/jest.config.js @@ -0,0 +1,54 @@ +process.env.TZ = 'GMT' + +const { compilerOptions } = require('./tsconfig') + +module.exports = { + globals: { + 'ts-jest': { + isolatedModules: true, + diagnostics: false + }, + __DEV__: false + }, + setupFilesAfterEnv: ['/scripts/jest/setup-file.js'], + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/index.tsx', + '!src/App.tsx', + '!src/bootstrap.tsx', + '!src/framework/strings/**', + '!src/services/**', + '!src/**/*.d.ts', + '!src/**/*.test.{ts,tsx}', + '!src/**/*.stories.{ts,tsx}', + '!src/**/__test__/**', + '!src/**/__tests__/**', + '!src/utils/test/**', + '!src/AppUtils.ts' + ], + coverageReporters: ['lcov', 'json-summary'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + '^.+\\.js$': 'ts-jest', + '^.+\\.ya?ml$': '/scripts/jest/yaml-transform.js', + '^.+\\.gql$': '/scripts/jest/gql-loader.js' + }, + moduleDirectories: ['node_modules', 'src'], + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + moduleNameMapper: { + '\\.s?css$': 'identity-obj-proxy', + 'monaco-editor': '/node_modules/react-monaco-editor', + '\\.(jpg|jpeg|png|gif|svg|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/scripts/jest/file-mock.js' + }, + coverageThreshold: { + global: { + statements: 60, + branches: 40, + functions: 40, + lines: 60 + } + }, + transformIgnorePatterns: ['node_modules/(?!(date-fns|lodash-es|p-debounce)/)'], + testPathIgnorePatterns: ['/dist'] +} diff --git a/web/jest.coverage.config.js b/web/jest.coverage.config.js new file mode 100644 index 000000000..1e8cbf44d --- /dev/null +++ b/web/jest.coverage.config.js @@ -0,0 +1,9 @@ +process.env.TZ = 'GMT' + +const config = require('./jest.config') +const { omit } = require('lodash') + +module.exports = { + ...omit(config, ['coverageThreshold', 'coverageReporters']), + coverageReporters: ['text-summary', 'json-summary'] +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 000000000..c6e388b47 --- /dev/null +++ b/web/package.json @@ -0,0 +1,176 @@ +{ + "name": "sample-module", + "description": "Harness Inc", + "version": "0.0.1", + "author": "Harness Inc", + "license": "Harness Inc", + "private": true, + "homepage": "http://harness.io/", + "repository": { + "type": "git", + "url": "https://github.com/drone/sample-module.git" + }, + "bugs": { + "url": "https://github.com/sample-module/sample-module/issues" + }, + "keywords": [], + "scripts": { + "dev": "NODE_ENV=development webpack serve --progress", + "test": "jest src --silent", + "test:watch": "jest --watch", + "lint": "eslint --rulesdir ./scripts/eslint-rules --ext .ts --ext .tsx src", + "typecheck": "tsc", + "clean": "rm -rf dist && rm -rf node_modules/.cache", + "services": "npm-run-all services:*", + "services:pm": "restful-react import --config restful-react.config.js pm", + "postservices": "prettier --write src/services/**/*.tsx", + "build": "npm run clean; webpack --mode production", + "coverage": "npm test --coverage", + "setup-github-registry": "sh scripts/setup-github-registry.sh", + "strings": "npm-run-all strings:*", + "strings:genTypes": "node scripts/strings/generateTypesCli.mjs", + "fmt": "prettier --write \"./src/**/*.{ts,tsx,css,scss}\"", + "micro:watch": "nodemon --watch 'src/**/*' -e ts,tsx,html,scss,svg,yaml --exec 'npm-run-all' -- micro:build micro:serve", + "micro:build": "webpack --mode production", + "micro:serve": "serve ./dist -l 3000" + }, + "dependencies": { + "@blueprintjs/core": "3.26.1", + "@blueprintjs/datetime": "3.13.0", + "@blueprintjs/select": "3.12.3", + "@harness/uicore": "^1.23.0", + "anser": "^2.1.0", + "classnames": "^2.3.1", + "clipboard-copy": "^3.1.0", + "formik": "1.5.8", + "immer": "^9.0.6", + "lodash-es": "^4.17.15", + "marked": "^3.0.8", + "masonry-layout": "^4.2.2", + "moment": "^2.25.3", + "monaco-editor": "^0.19.2", + "qs": "^6.9.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-draggable": "^4.4.2", + "react-router-dom": "5.2.0", + "react-table": "^7.1.0", + "restful-react": "15.6.0", + "swr": "^0.5.4", + "yaml": "^1.10.0" + }, + "devDependencies": { + "@harness/css-types-loader": "^3.1.0", + "@harness/jarvis": "0.12.0", + "@testing-library/jest-dom": "^5.12.0", + "@testing-library/react": "^10.0.3", + "@testing-library/react-hooks": "5", + "@testing-library/user-event": "^10.3.1", + "@types/classnames": "^2.2.10", + "@types/lodash-es": "^4.17.3", + "@types/mustache": "^4.0.1", + "@types/path-to-regexp": "^1.7.0", + "@types/qs": "^6.9.4", + "@types/react": "^17.0.3", + "@types/react-dom": "^17.0.3", + "@types/react-router-dom": "^5.1.7", + "@types/react-table": "^7.0.18", + "@types/testing-library__react-hooks": "^3.2.0", + "@typescript-eslint/eslint-plugin": "^4.22.0", + "@typescript-eslint/parser": "^4.22.0", + "case": "^1.6.3", + "circular-dependency-plugin": "^5.2.2", + "css-loader": "^6.3.0", + "dotenv": "^10.0.0", + "eslint": "^7.27.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^2.4.0", + "eslint-plugin-import": "^2.23.3", + "eslint-plugin-jest": "^24.3.6", + "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react-hooks": "^4.2.0", + "fast-json-stable-stringify": "^2.1.0", + "file-loader": "^6.2.0", + "fork-ts-checker-webpack-plugin": "^6.2.1", + "glob": "^7.1.6", + "html-webpack-plugin": "^5.3.1", + "jest": "^26.2.0", + "lighthouse": "^6.5.0", + "lint-staged": "^11.0.0", + "mini-css-extract-plugin": "^2.4.2", + "monaco-editor-webpack-plugin": "1.8.*", + "mustache": "^4.0.1", + "nodemon": "^2.0.15", + "npm-run-all": "^4.1.5", + "path-to-regexp": "^6.1.0", + "prettier": "^2.3.2", + "react-test-renderer": "^17.0.2", + "sass": "^1.32.8", + "sass-loader": "^12.1.0", + "serve": "^13.0.2", + "style-loader": "^3.3.0", + "ts-jest": "^26.5.5", + "ts-loader": "^9.2.6", + "tsconfig-paths-webpack-plugin": "^3.5.1", + "typescript": "^4.2.4", + "url-loader": "^4.1.1", + "webpack": "^5.58.0", + "webpack-bundle-analyzer": "^4.4.0", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.6.0", + "yaml-loader": "^0.6.0" + }, + "resolutions": { + "@types/react": "^17.0.3", + "@types/react-dom": "^17.0.3", + "@types/testing-library__react": "^10.0.0", + "@types/testing-library__dom": "^7.0.0", + "anser": "2.0.1", + "create-react-context": "0.3.0" + }, + "engines": { + "node": ">=14.16.0" + }, + "nodemonConfig": { + "verbose": true, + "ignore": [ + "dist/*", + "__tests__/*", + ".vscode/*", + "coverage/*", + "scripts/*", + "**/*.d.ts", + "src/framework/strings/stringTypes.ts" + ], + "delay": 2500 + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --rulesdir ./scripts/eslint-rules --resolve-plugins-relative-to", + "sh scripts/typecheck-staged.sh", + "prettier --check" + ], + "*.scss": [ + "prettier --check" + ], + "strings.*.yaml": [ + "npm strings:check" + ], + "webpack.devServerProxy.config.js": [ + "exit 1" + ] + }, + "i18nSettings": { + "extensionToLanguageMap": { + "es": [ + "es" + ], + "en": [ + "en", + "en-US", + "en-IN", + "en-UK" + ] + } + } +} diff --git a/web/restful-react.config.js b/web/restful-react.config.js new file mode 100644 index 000000000..3fe478bc0 --- /dev/null +++ b/web/restful-react.config.js @@ -0,0 +1,17 @@ +/** + * Please match the config key to the directory under services. + * This is required for the transform to work + */ +const customGenerator = require('./scripts/swagger-custom-generator.js') + +module.exports = { + pm: { + output: 'src/services/pm/index.tsx', + file: 'src/services/pm/swagger.json', + transformer: 'scripts/swagger-transform.js', + customImport: `import { getConfig } from "../config";`, + customProps: { + base: `{getConfig("pm/api/v1")}` + } + } +} diff --git a/web/scripts/clean-css-types.js b/web/scripts/clean-css-types.js new file mode 100644 index 000000000..91059e169 --- /dev/null +++ b/web/scripts/clean-css-types.js @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-var-requires, no-console */ + +/** + * Since all the ".css.d.ts" files are generated automatically from webpack, developers might be + * aware of its existence. + * + * Example: "MyAwesomeComponent.css" file will have a "MyAwesomeComponent.css.d.ts" file + * + * When a ".css" file is deleted, the corresponding ".css.d.ts" file must be deleted too, but this is + * hard to do in a fast-paced development environment. + * + * The rationale here is that, since these files are generated automatically, they must be cleaned + * automatically too. + * + * How do we do it? + * We glob for all the ".css.d.ts" files and check if it has a corresponding ".css" file + * If it doesn't, we delete that ".css.d.ts" file + */ + +const fs = require('fs'); +const glob = require('glob'); + +const files = glob.sync('src/**/*.css.d.ts'); +console.log(`Found ${files.length} '.css.d.ts' files`); + +let i = 0; + +files.forEach(file => { + // for every '.css' there will be a coresponding '.css.d.ts' file and vice versa + const cssFile = file.replace('.d.ts', ''); + + if (!fs.existsSync(cssFile)) { + console.log(`Deleting "${file}" because corresponding "${cssFile}" does not exist`); + fs.unlinkSync(file); + i++; + } +}); + +console.log(`Deleted total of ${i} '.css.d.ts' files`); diff --git a/web/scripts/eslint-rules/duplicate-data-tooltip-id.js b/web/scripts/eslint-rules/duplicate-data-tooltip-id.js new file mode 100644 index 000000000..d81a5c645 --- /dev/null +++ b/web/scripts/eslint-rules/duplicate-data-tooltip-id.js @@ -0,0 +1,27 @@ +const { get } = require('lodash') +const toolTipValuesMap = {} +module.exports = { + meta: { + docs: { + description: `Give warning for duplicate tooltip id's'` + } + }, + + create: function (context) { + return { + JSXAttribute(node) { + if (get(node, 'name.name') === 'data-tooltip-id' && get(node, 'value.type') === 'Literal') { + if (toolTipValuesMap[get(node, 'value.value')]) { + return context.report({ + node, + message: 'Duplicate tooltip id' + }) + } else { + toolTipValuesMap[get(node, 'value.value')] = true + } + } + return null + } + } + } +} diff --git a/web/scripts/eslint-rules/jest-no-mock.js b/web/scripts/eslint-rules/jest-no-mock.js new file mode 100644 index 000000000..8c8978811 --- /dev/null +++ b/web/scripts/eslint-rules/jest-no-mock.js @@ -0,0 +1,43 @@ +const { get } = require('lodash') + +module.exports = { + meta: { + schema: [ + { + type: 'object', + properties: { + module: { + type: 'object' + } + }, + additionalProperties: false + } + ], + docs: { + description: `Restrict some properties from being mocked in jest` + } + }, + + create: function (context) { + return { + CallExpression(node) { + const moduleList = context.options[0].module + if ( + get(node, 'callee.type') === 'MemberExpression' && + get(node, 'callee.object.type') === 'Identifier' && + get(node, 'callee.object.name') === 'jest' && + get(node, 'callee.property.name') === 'mock' && + get(node, 'arguments[0].type') === 'Literal' && + moduleList.hasOwnProperty(get(node, 'arguments[0].value')) + ) { + const errorMessage = moduleList[get(node, 'arguments[0].value')] + return context.report({ + node, + message: errorMessage + }) + } + return null + } + } + } +} diff --git a/web/scripts/eslint-rules/no-document-body-snapshot.js b/web/scripts/eslint-rules/no-document-body-snapshot.js new file mode 100644 index 000000000..ac82d1ab4 --- /dev/null +++ b/web/scripts/eslint-rules/no-document-body-snapshot.js @@ -0,0 +1,28 @@ +const { get } = require('lodash') + +module.exports = { + meta: { + docs: { + description: `Give warning for statements 'expect(document.body).toMatchSnapshot()'` + } + }, + + create: function (context) { + return { + CallExpression(node) { + if ( + get(node, 'callee.object.callee.name') === 'expect' && + get(node, 'callee.object.arguments[0].object.name') === 'document' && + get(node, 'callee.object.arguments[0].property.name') === 'body' && + get(node, 'callee.property.name') === 'toMatchSnapshot' + ) { + return context.report({ + node, + message: 'document.body match snapshot not allowed' + }) + } + return null + } + } + } +} diff --git a/web/scripts/jest/file-mock.js b/web/scripts/jest/file-mock.js new file mode 100644 index 000000000..0e56c5b5f --- /dev/null +++ b/web/scripts/jest/file-mock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/web/scripts/jest/gql-loader.js b/web/scripts/jest/gql-loader.js new file mode 100644 index 000000000..77e214886 --- /dev/null +++ b/web/scripts/jest/gql-loader.js @@ -0,0 +1,10 @@ +module.exports = { + process(src) { + return ( + 'module.exports = ' + + JSON.stringify(src) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029') + ) + } +} diff --git a/web/scripts/jest/setup-file.js b/web/scripts/jest/setup-file.js new file mode 100644 index 000000000..7467d77aa --- /dev/null +++ b/web/scripts/jest/setup-file.js @@ -0,0 +1,43 @@ +import '@testing-library/jest-dom' +import { setAutoFreeze, enableMapSet } from 'immer' +import { noop } from 'lodash-es' + +// set up Immer +setAutoFreeze(false) +enableMapSet() + +process.env.TZ = 'UTC' + +document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + commonAncestorContainer: { + nodeName: 'BODY', + ownerDocument: document, + }, +}) +window.HTMLElement.prototype.scrollIntoView = jest.fn() +window.scrollTo = jest.fn() + +window.fetch = jest.fn((url, options) => { + fail(`A fetch is being made to url '${url}' with options: +${JSON.stringify(options, null, 2)} +Please mock this call.`) + throw new Error() +}) + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}) + +jest.mock('react-timeago', () => () => 'dummy date') diff --git a/web/scripts/jest/yaml-transform.js b/web/scripts/jest/yaml-transform.js new file mode 100644 index 000000000..2d169bf14 --- /dev/null +++ b/web/scripts/jest/yaml-transform.js @@ -0,0 +1,9 @@ +const yaml = require('yaml') + +module.exports = { + process(src) { + const json = yaml.parse(src) + + return { code: `module.exports = ${JSON.stringify(json)}` } + } +} diff --git a/web/scripts/lighthouse/lighthouse.js b/web/scripts/lighthouse/lighthouse.js new file mode 100644 index 000000000..b5616186a --- /dev/null +++ b/web/scripts/lighthouse/lighthouse.js @@ -0,0 +1,214 @@ +const puppeteer = require('puppeteer') +const lighthouse = require('lighthouse') +const reportGenerator = require('lighthouse/lighthouse-core/report/report-generator') +const QA_URL = 'https://qa.harness.io/ng/' +const PROD_URL = 'https://app.harness.io/ng/' +const fs = require('fs') +const lighthouseRunTimes = 3 +const acceptableChange = process.env.LIGHT_HOUSE_ACCEPTANCE_CHANGE + ? parseInt(process.env.LIGHT_HOUSE_ACCEPTANCE_CHANGE) + : 5 +console.log('acceptableChange', acceptableChange) +const PORT = 8041 +let url = PROD_URL +let isQA = false +let passWord = '' +let emailId = 'ui_perf_test_prod@mailinator.com' +async function run() { + if (process.argv[2] === 'qa') { + isQA = true + url = QA_URL + passWord = process.env.PASSWORD + } else { + passWord = process.env.LIGHT_HOUSE_SECRET + } + if (!url) { + throw 'Please provide URL as a first argument' + } + + const getScores = resultSupplied => { + let json = reportGenerator.generateReport(resultSupplied.lhr, 'json') + json = JSON.parse(json) + let scores = { + Performance: 0, + Accessibility: 0, + 'Best Practices': 0, + SEO: 0, + 'Time To Interactive': 0, + 'First ContentFul Paint': 0, + 'First Meaningful Paint': 0 + } + scores.Performance = parseFloat(json.categories.performance.score) * 100 + scores.Accessibility = parseFloat(json.categories.accessibility.score) * 100 + scores['Best Practices'] = parseFloat(json['categories']['best-practices']['score']) * 100 + scores.SEO = parseFloat(json.categories.seo.score) * 100 + scores['Time To Interactive'] = json.audits.interactive.displayValue + scores['First Meaningful Paint'] = json.audits['first-meaningful-paint'].displayValue + scores['First ContentFul Paint'] = json.audits['first-contentful-paint'].displayValue + console.log(scores) + return scores + } + + const runLightHouseNtimes = async (n, passedUrl) => { + let localResults = [] + for (let i = 0; i < n; i++) { + console.log(`Running lighthouse on ${passedUrl} for the ${i + 1} time`) + try { + const result = await lighthouse(passedUrl, { port: PORT, disableStorageReset: true }) + localResults.push(getScores(result)) + } catch (e) { + console.log(e) + process.exit(1) + } + } + return localResults + } + const getAverageResult = (listOfResults, attributeName) => { + let listLength = listOfResults.length + let returnAvg = 0 + if (listLength) { + const sum = listOfResults.reduce((tempSum, ele) => { + tempSum = tempSum + parseFloat(ele[attributeName]) + return tempSum + }, returnAvg) + return sum / listLength + } + return returnAvg + } + const getFilterResults = resultsToBeFilterd => { + return { + Performance: getAverageResult(resultsToBeFilterd, 'Performance').toFixed(2), + Accessibility: getAverageResult(resultsToBeFilterd, 'Accessibility').toFixed(2), + 'Best Practices': getAverageResult(resultsToBeFilterd, 'Best Practices').toFixed(2), + SEO: getAverageResult(resultsToBeFilterd, 'SEO').toFixed(2), + 'Time To Interactive': `${getAverageResult(resultsToBeFilterd, 'Time To Interactive').toFixed(2)} s`, + 'First Meaningful Paint': `${getAverageResult(resultsToBeFilterd, 'First Meaningful Paint').toFixed(2)} s`, + 'First ContentFul Paint': `${getAverageResult(resultsToBeFilterd, 'First ContentFul Paint').toFixed(2)} s` + } + } + const runLightHouseNtimesAndGetResults = async (numberOfTimes, passedUrl) => { + const browser = await puppeteer.launch({ + headless: true, + executablePath: '/usr/bin/google-chrome', + args: ['--no-sandbox', `--remote-debugging-port=${PORT}`] + }) + let page = await browser.newPage() + await page.setDefaultNavigationTimeout(300000) // 5 minutes timeout + await page.goto(passedUrl) + const emailInput = await page.$('#email') + await emailInput.type(emailId) + const passwordInput = await page.$('#password') + await passwordInput.type(passWord) + await page.$eval('input[type="submit"]', form => form.click()) + await page.waitForNavigation() + await page.waitForXPath("//span[text()='Main Dashboard']") + let results = await runLightHouseNtimes(numberOfTimes, passedUrl) + await browser.close() + return getFilterResults(results) + } + const percentageChangeInTwoParams = (dataToBeCompared, benchMarkData, parameter) => { + const percentageChange = parseFloat( + ((parseFloat(dataToBeCompared) - parseFloat(benchMarkData)) / parseFloat(benchMarkData)) * 100 + ).toFixed(2) + console.log( + `Comparing ${parameter} Benchmark Value:${benchMarkData}, Data to be compared Value: ${dataToBeCompared} precentage change: ${percentageChange}` + ) + return percentageChange + } + let finalResults = await runLightHouseNtimesAndGetResults(lighthouseRunTimes, url) + + console.log(`Scores for the ${url} \n`, finalResults) + const finalReport = `Lighthouse ran ${lighthouseRunTimes} times on (${url}) and following are the results + Name | Value +------------ | ------------- +Performance | ${finalResults.Performance}/100 +SEO | ${finalResults.SEO}/100 +Accessibility | ${finalResults.Accessibility}/100 +Best Practices | ${finalResults['Best Practices']}/100 +First ContentFul Paint | ${finalResults['First ContentFul Paint']} +First Meaningful Paint | ${finalResults['First Meaningful Paint']} +Time To Interactive | ${finalResults['Time To Interactive']}` + if (!isQA) { + fs.writeFile('lighthouse.md', finalReport, function (err) { + if (err) { + console.log(err) + process.exit(1) + } + }) + } else { + console.log('Final Report:', finalReport) + + console.log(`Starting benchmark results collection using ${PROD_URL}`) + let benchMark = await runLightHouseNtimesAndGetResults(lighthouseRunTimes, PROD_URL) + console.log(`benchmark results`, benchMark) + let hasError = false + let percentChange = percentageChangeInTwoParams(finalResults.Performance, benchMark.Performance, 'Performance') + if (percentChange < -acceptableChange) { + console.error( + `Performance value of ${finalResults.Performance} is ${percentChange} % less than expected ${benchMark.Performance}` + ) + hasError = true + } + percentChange = percentageChangeInTwoParams(finalResults.SEO, benchMark.SEO, 'SEO') + if (percentChange < -acceptableChange) { + console.error(`SEO value ${finalResults.SEO} is ${percentChange} % less than expected ${benchMark.SEO}`) + hasError = true + } + percentChange = percentageChangeInTwoParams(finalResults.Accessibility, benchMark.Accessibility, 'Accessibility') + if (percentChange < -acceptableChange) { + console.error( + `Accessibility value ${finalResults.Accessibility} is ${percentChange} % less than expected ${benchMark.Accessibility}` + ) + hasError = true + } + percentChange = percentageChangeInTwoParams( + finalResults['Best Practices'], + benchMark['Best Practices'], + 'Best Practices' + ) + if (percentChange < -acceptableChange) { + console.error( + `Best Practices value ${finalResults['Best Practices']} is ${percentChange} % less than expected ${benchMark['Best Practices']}` + ) + hasError = true + } + percentChange = percentageChangeInTwoParams( + finalResults['First ContentFul Paint'], + benchMark['First ContentFul Paint'], + 'First ContentFul Paint' + ) + if (percentChange > acceptableChange) { + console.error( + `First ContentFul Paint value ${finalResults['First ContentFul Paint']} is ${percentChange} % more than expected ${benchMark['First ContentFul Paint']}` + ) + hasError = true + } + percentChange = percentageChangeInTwoParams( + finalResults['First Meaningful Paint'], + benchMark['First Meaningful Paint'], + 'First Meaningful Paint' + ) + if (percentChange > acceptableChange) { + console.error( + `First Meaningful Paint value ${finalResults['First Meaningful Paint']} is ${percentChange} % more than expected ${benchMark['First Meaningful Paint']}` + ) + hasError = true + } + percentChange = percentageChangeInTwoParams( + finalResults['Time To Interactive'], + benchMark['Time To Interactive'], + 'Time To Interactive' + ) + if (percentChange > acceptableChange) { + console.error( + `Time To Interactive value ${finalResults['Time To Interactive']} is ${percentChange} % more than expected ${benchMark['Time To Interactive']}` + ) + hasError = true + } + if (hasError) { + console.log('Failed in benchmark comparison') + process.exit(1) + } + } +} +run() diff --git a/web/scripts/setup-github-registry.sh b/web/scripts/setup-github-registry.sh new file mode 100644 index 000000000..345b52c5b --- /dev/null +++ b/web/scripts/setup-github-registry.sh @@ -0,0 +1,21 @@ +echo +echo "Setting up GitHub Package Registry" +echo "----------------------------------" +echo +echo "Follow these steps to get access to GitHub Package Registry where" +echo "Harness modules are published privately." +echo +echo "1. Go to https://github.com/settings/tokens - Sign in with your Harness Github account if needed" +echo "2. Create a token with 'repo' and 'read:packages' scopes" +read -s -p "3. Copy the token and paste it here: " githubToken +echo +echo +echo "All done. Token is saved in ~/.npmrc." + +echo "@harness:registry=https://npm.pkg.github.com" > ~/.npmrc +echo "//npm.pkg.github.com/:_authToken="$githubToken >> ~/.npmrc +echo "always-auth=true" >> ~/.npmrc + +echo +echo "Update yarn checksums...." +yarn --update-checksums \ No newline at end of file diff --git a/web/scripts/strings/generateTypes.cjs b/web/scripts/strings/generateTypes.cjs new file mode 100644 index 000000000..7d4a23ca0 --- /dev/null +++ b/web/scripts/strings/generateTypes.cjs @@ -0,0 +1,63 @@ +const path = require('path') +const fs = require('fs') + +const yaml = require('yaml') +const _ = require('lodash') +const glob = require('glob') + +const runPrettier = require('../utils/runPrettier.cjs') + +function flattenKeys(data, parentPath = []) { + const keys = [] + + _.keys(data).forEach(key => { + const value = data[key] + const newPath = [...parentPath, key] + + if (Array.isArray(value)) { + throw new TypeError(`Array is not supported in strings.yaml\nPath: "${newPath.join('.')}"`) + } + + if (_.isPlainObject(data[key])) { + keys.push(...flattenKeys(data[key], [...parentPath, key])) + } else { + keys.push([...parentPath, key].join('.')) + } + }) + + keys.sort() + + return keys +} + +async function generateTypes() { + const i18nContent = await fs.promises.readFile(path.resolve(process.cwd(), `src/i18n/strings.en.yaml`), 'utf8') + + const allData = [ + { + moduleRef: null, + keys: flattenKeys(yaml.parse(i18nContent)) + } + ] + + let content = ` +/** + * This file is auto-generated. Please do not modify this file manually. + * Use the command \`yarn strings\` to regenerate this file. + */ +export interface StringsMap {` + + allData + .flatMap(({ keys }) => keys) + .forEach(key => { + content += `\n '${key}': string` + }) + + content += `\n}` + + content = await runPrettier(content, 'typescript') + + await fs.promises.writeFile(path.resolve(process.cwd(), 'src/framework/strings/stringTypes.ts'), content, 'utf8') +} + +module.exports = generateTypes diff --git a/web/scripts/strings/generateTypesCli.mjs b/web/scripts/strings/generateTypesCli.mjs new file mode 100644 index 000000000..913c163f7 --- /dev/null +++ b/web/scripts/strings/generateTypesCli.mjs @@ -0,0 +1,5 @@ +import generateTypes from './generateTypes.cjs' + +await generateTypes() + +console.log('✅ Generated type for string files succesfully!') diff --git a/web/scripts/swagger-custom-generator.js b/web/scripts/swagger-custom-generator.js new file mode 100644 index 000000000..cb793f2e3 --- /dev/null +++ b/web/scripts/swagger-custom-generator.js @@ -0,0 +1,21 @@ +const { camel } = require("case"); + +module.exports = ({ componentName, verb, route, description, genericsTypes, paramsInPath, paramsTypes }, basePath) => { + const propsType = type => + `${type}UsingFetchProps<${genericsTypes}>${paramsInPath.length ? ` & {${paramsTypes}}` : ""}`; + + if (verb === "get") { + return `${description}export const ${camel(componentName)}Promise = (${ + paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props" + }: ${propsType( + "Get", + )}, signal?: RequestInit["signal"]) => getUsingFetch<${genericsTypes}>(${basePath}, \`${route}\`, props, signal);\n\n` + } + else { + return `${description}export const ${camel(componentName)}Promise = (${ + paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props" + }: ${propsType( + "Mutate", + )}, signal?: RequestInit["signal"]) => mutateUsingFetch<${genericsTypes}>("${verb.toUpperCase()}", ${basePath}, \`${route}\`, props, signal);\n\n`; + } +} \ No newline at end of file diff --git a/web/scripts/swagger-transform.js b/web/scripts/swagger-transform.js new file mode 100644 index 000000000..b27d45fad --- /dev/null +++ b/web/scripts/swagger-transform.js @@ -0,0 +1,45 @@ +const fs = require('fs') +const path = require('path') +const _ = require('lodash') +const yaml = require('js-yaml') +const stringify = require('fast-json-stable-stringify') + +module.exports = inputSchema => { + const argv = process.argv.slice(2) + const config = argv[0] + + if (config) { + const overridesFile = path.join('src/services', config, 'overrides.yaml') + const transformFile = path.join('src/services', config, 'transform.js') + + let paths = inputSchema.paths + + if (fs.existsSync(overridesFile)) { + const data = fs.readFileSync(overridesFile, 'utf8') + const { allowpaths, operationIdOverrides } = yaml.safeLoad(data) + + if (!allowpaths.includes('*')) { + paths = _.pick(paths, ...allowpaths) + } + + _.forIn(operationIdOverrides, (value, key) => { + const [path, method] = key.split('.') + + if (path && method && _.has(paths, path) && _.has(paths[path], method)) { + _.set(paths, [path, method, 'operationId'], value) + } + }) + } + + inputSchema.paths = paths + + if (fs.existsSync(transformFile)) { + const transform = require(path.resolve(process.cwd(), transformFile)) + + inputSchema = transform(inputSchema) + } + } + + // stringify and parse json to get a stable object + return JSON.parse(stringify(inputSchema)) +} diff --git a/web/scripts/utils/runPrettier.cjs b/web/scripts/utils/runPrettier.cjs new file mode 100644 index 000000000..5e3b9e0d6 --- /dev/null +++ b/web/scripts/utils/runPrettier.cjs @@ -0,0 +1,17 @@ +const fs = require('fs') +const path = require('path') + +const prettier = require('prettier') + +/** + * Run prettier on given content using the specified parser + * @param content {String} + * @param parser {String} + */ +async function runPrettier(content, parser) { + const prettierConfig = await prettier.resolveConfig(process.cwd()) + + return prettier.format(content, { ...prettierConfig, parser }) +} + +module.exports = runPrettier diff --git a/web/scripts/webpack/GenerateStringTypesPlugin.js b/web/scripts/webpack/GenerateStringTypesPlugin.js new file mode 100644 index 000000000..7c2a3e35b --- /dev/null +++ b/web/scripts/webpack/GenerateStringTypesPlugin.js @@ -0,0 +1,18 @@ +const generateStringTypes = require('../strings/generateTypes.cjs') + +class GenerateStringTypesPlugin { + apply(compiler) { + compiler.hooks.emit.tapAsync('GenerateStringTypesPlugin', (compilation, callback) => { + try { + generateStringTypes().then( + () => callback(), + e => callback(e) + ) + } catch (e) { + callback(e) + } + }) + } +} + +module.exports.GenerateStringTypesPlugin = GenerateStringTypesPlugin diff --git a/web/src/App.scss b/web/src/App.scss new file mode 100644 index 000000000..5e5863a57 --- /dev/null +++ b/web/src/App.scss @@ -0,0 +1,14 @@ +@import '~normalize.css'; +@import '~@blueprintjs/core/lib/css/blueprint.css'; +@import '~@blueprintjs/datetime/lib/css/blueprint-datetime.css'; +@import '~@harness/uicore/dist/index.css'; + +html, +body, +#react-root { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + background-color: var(--white); +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 000000000..eee735c57 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState, useCallback } from 'react' +import { RestfulProvider } from 'restful-react' +import { TooltipContextProvider, ModalProvider } from '@harness/uicore' +import { FocusStyleManager } from '@blueprintjs/core' +import AppErrorBoundary from 'framework/AppErrorBoundary/AppErrorBoundary' +import { useAPIToken } from 'hooks/useAPIToken' +import { AppContextProvider } from 'AppContext' +import { setBaseRouteInfo } from 'RouteUtils' +import type { AppProps } from 'AppProps' +import { buildResfulReactRequestOptions, handle401 } from 'AppUtils' +import { RouteDestinations } from 'RouteDestinations' +import { languageLoader } from './framework/strings/languageLoader' +import type { LanguageRecord } from './framework/strings/languageLoader' +import { StringsContextProvider } from './framework/strings/StringsContextProvider' +import './App.scss' + +FocusStyleManager.onlyShowFocusOnTabs() + +const App: React.FC = props => { + const { + standalone = false, + accountId = '', + baseRoutePath = '', + lang = 'en', + apiToken, + on401 = handle401, + children, + hooks = {}, + components = {} + } = props + const [strings, setStrings] = useState() + const [token, setToken] = useAPIToken(apiToken) + const getRequestOptions = useCallback((): Partial => { + return buildResfulReactRequestOptions(token) + }, [token]) + setBaseRouteInfo(accountId, baseRoutePath) + + useEffect(() => { + languageLoader(lang).then(setStrings) + }, [lang, setStrings]) + + useEffect(() => { + if (!apiToken) { + setToken(token) + } + }, [apiToken, token, setToken]) + + return strings ? ( + + + + { + if (!response.ok && response.status === 401) { + on401() + } + }}> + + {children ? children : } + + + + + + ) : null +} + +export default App diff --git a/web/src/AppContext.tsx b/web/src/AppContext.tsx new file mode 100644 index 000000000..310d870bd --- /dev/null +++ b/web/src/AppContext.tsx @@ -0,0 +1,32 @@ +import React, { useState, useContext } from 'react' +import { noop } from 'lodash-es' +import type { AppProps } from 'AppProps' + +interface AppContextProps extends AppProps { + setAppContext: (value: Partial) => void +} + +const AppContext = React.createContext({ + standalone: true, + setAppContext: noop, + hooks: {}, + components: {} +}) + +export const AppContextProvider: React.FC<{ value: AppProps }> = ({ value: initialValue, children }) => { + const [appStates, setAppStates] = useState(initialValue) + + return ( + { + setAppStates({ ...appStates, ...props }) + } + }}> + {children} + + ) +} + +export const useAppContext: () => AppContextProps = () => useContext(AppContext) diff --git a/web/src/AppProps.ts b/web/src/AppProps.ts new file mode 100644 index 000000000..a39a33706 --- /dev/null +++ b/web/src/AppProps.ts @@ -0,0 +1,78 @@ +import type React from 'react' +import type { LangLocale } from './framework/strings/languageLoader' + +/** + * AppProps defines an interface for host (parent) and + * child (micro-frontend) apps to talk to each other. It allows behaviors + * of the child app to be customized from the parent app. + * + * Areas of customization: + * + * - API token + * - Active user + * - Active locale (i18n) + * - Global error handling (like 401) + * - etc... + * + * Under standalone mode, the micro-frontend app uses default + * implementation of the interface in AppUtils.ts. + * + * This interface is published to allow parent to do type checking. + */ +export interface AppProps { + /** Flag to tell if App is mounted as a standalone app */ + standalone: boolean + + /** App children. When provided, children is a remote view which will be mounted under App contexts */ + children?: React.ReactNode + + /** Base Route information where app is mounted */ + baseRoutePath?: string + + /** Active account id when app is embedded */ + accountId?: string + + /** Language to use in the app, default is 'en' */ + lang?: LangLocale + + /** API token to be used in Restful React */ + apiToken?: string + + /** 401 handler. Used in parent app to override 401 handling from child app */ + on401?: () => void + + /** React Hooks that Harness Platform passes down. Note: Pass only hooks that your app need */ + hooks: Partial + + /** React Components that Harness Platform passes down. Note: Pass only components that your app need */ + components: Partial +} + +/** + * AppPathProps defines all possible URL parameters that application accepts. + */ +export interface AppPathProps { + accountId?: string + orgIdentifier?: string + projectIdentifier?: string + module?: string + policyIdentifier?: string + policySetIdentifier?: string + evaluationId?: string + pipeline?: string + execution?: string +} + +/** + * AppPropsHook defines a collection of React Hooks that application receives from + * Platform integration. + */ +export interface AppPropsHook {} // eslint-disable-line @typescript-eslint/no-empty-interface + +/** + * AppPropsComponent defines a collection of React Components that application receives from + * Platform integration. + */ +export interface AppPropsComponent { + NGBreadcrumbs: React.FC +} diff --git a/web/src/AppUtils.ts b/web/src/AppUtils.ts new file mode 100644 index 000000000..8cc69a3fd --- /dev/null +++ b/web/src/AppUtils.ts @@ -0,0 +1,32 @@ +/** + * Handle 401 error from API. + * + * This function is called to handle 401 (unauthorized) API calls under standalone mode. + * In embedded mode, the parent app is responsible to pass its handler down. + * + * Mostly, the implementation of this function is just a redirection to signin page. + */ +export function handle401(): void { + // eslint-disable-next-line no-console + console.error('TODO: Handle 401 error...') +} + +/** + * Build Restful React Request Options. + * + * This function is an extension to configure HTTP headers before passing to Restful + * React to make an API call. Customizations to fulfill the micro-frontend backend + * service happen here. + * + * @param token API token + * @returns Resful React RequestInit object. + */ +export function buildResfulReactRequestOptions(token?: string): Partial { + const headers: RequestInit['headers'] = {} + + if (token?.length) { + headers.Authorization = `Bearer ${token}` + } + + return { headers } +} diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts new file mode 100644 index 000000000..361540bfc --- /dev/null +++ b/web/src/RouteDefinitions.ts @@ -0,0 +1,39 @@ +import { toRouteURL } from 'RouteUtils' +import type { AppPathProps } from 'AppProps' + +export enum RoutePath { + SIGNIN = '/signin', + TEST_PAGE1 = '/test-page1', + TEST_PAGE2 = '/test-page2', + + REGISTER = '/register', + LOGIN = '/login', + USERS = '/users', + ACCOUNT = '/account', + PIPELINES = '/pipelines', + PIPELINE = '/pipelines/:pipeline', + PIPELINE_SETTINGS = '/pipelines/:pipeline/settings', + PIPELINE_EXECUTIONS = '/pipelines/:pipeline/executions', + PIPELINE_EXECUTION = '/pipelines/:pipeline/executions/:execution', + PIPELINE_EXECUTION_SETTINGS = '/pipelines/:pipeline/executions/:execution/settings' +} + +export default { + toLogin: (): string => toRouteURL(RoutePath.LOGIN), + toRegister: (): string => toRouteURL(RoutePath.REGISTER), + toAccount: (): string => toRouteURL(RoutePath.ACCOUNT), + toPipelines: (): string => toRouteURL(RoutePath.PIPELINES), + toPipeline: ({ pipeline }: Required>): string => + toRouteURL(RoutePath.PIPELINE, { pipeline }), + toPipelineExecutions: ({ pipeline }: Required>): string => + toRouteURL(RoutePath.PIPELINE_EXECUTIONS, { pipeline }), + toPipelineSettings: ({ pipeline }: Required>): string => + toRouteURL(RoutePath.PIPELINE_SETTINGS, { pipeline }), + toPipelineExecution: ({ pipeline, execution }: AppPathProps): string => + toRouteURL(RoutePath.PIPELINE_EXECUTION, { pipeline, execution }), + toPipelineExecutionSettings: ({ pipeline, execution }: AppPathProps): string => + toRouteURL(RoutePath.PIPELINE_EXECUTION_SETTINGS, { pipeline, execution }) + + // @see https://github.com/drone/policy-mgmt/blob/main/web/src/RouteDefinitions.ts + // for more examples regarding to passing parameters to generate URLs +} diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx new file mode 100644 index 000000000..8ef8a7aa2 --- /dev/null +++ b/web/src/RouteDestinations.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { HashRouter, Route, Switch } from 'react-router-dom' +import type { AppProps } from 'AppProps' +import { NotFoundPage } from 'pages/404/NotFoundPage' +import { routePath } from 'RouteUtils' +import { RoutePath } from 'RouteDefinitions' + +import { Login } from './pages/Login/Login' +import { Home } from './pages/Pipelines/Pipelines' +import { Executions } from './pages/Executions/Executions' +import { ExecutionSettings } from './pages/Execution/Settings' +import { PipelineSettings } from './pages/Pipeline/Settings' +import { Account } from './pages/Account/Account' +import { SideNav } from './components/SideNav/SideNav' + +export const RouteDestinations: React.FC> = ({ standalone }) => { + // TODO: Add a generic Auth Wrapper + + const Destinations: React.FC = () => ( + + {standalone && ( + + + + )} + {standalone && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + + return standalone ? ( + + + + ) : ( + + ) +} diff --git a/web/src/RouteUtils.ts b/web/src/RouteUtils.ts new file mode 100644 index 000000000..e1acc32d0 --- /dev/null +++ b/web/src/RouteUtils.ts @@ -0,0 +1,59 @@ +import { generatePath } from 'react-router-dom' +import type { AppPathProps } from 'AppProps' + +let baseRoutePath: string +let accountId: string + +export const setBaseRouteInfo = (_accountId: string, _baseRoutePath: string): void => { + accountId = _accountId + baseRoutePath = _baseRoutePath +} + +type Scope = Pick + +// +// Note: This function needs to be in sync with NextGen UI's routeUtils' getScopeBasedRoute. When +// it's out of sync, the URL routing scheme could be broken. +// @see https://github.com/harness/harness-core-ui/blob/master/src/modules/10-common/utils/routeUtils.ts#L171 +// +const getScopeBasedRouteURL = ({ path, scope = {} }: { path: string; scope?: Scope }): string => { + if (window.APP_RUN_IN_STANDALONE_MODE) { + return path + } + + const { orgIdentifier, projectIdentifier, module } = scope + + // + // TODO: Change this scheme below to reflect your application when it's embedded into Harness NextGen UI + // + + // The Sample Module UI app is mounted in three places in Harness Platform + // 1. Account Settings (account level) + // 2. Org Details (org level) + // 3. Project Settings (project level) + if (module && orgIdentifier && projectIdentifier) { + return `/account/${accountId}/${module}/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/sample-module${path}` + } else if (orgIdentifier && projectIdentifier) { + return `/account/${accountId}/home/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/sample-module${path}` + } else if (orgIdentifier) { + return `/account/${accountId}/settings/organizations/${orgIdentifier}/setup/sample-module${path}` + } + + return `/account/${accountId}/settings/sample-module${path}` +} + +/** + * Generate route path to be used in RouteDefinitions. + * @param path route path + * @returns a proper route path that works in both standalone and embedded modes. + */ +export const routePath = (path: string): string => `${baseRoutePath || ''}${path}` + +/** + * Generate route URL to be used RouteDefinitions' default export (aka actual react-router link href) + * @param path route path + * @param params URL parameters + * @returns a proper URL that works in both standalone and embedded modes. + */ +export const toRouteURL = (path: string, params?: AppPathProps): string => + generatePath(getScopeBasedRouteURL({ path, scope: params }), { ...params, accountId }) diff --git a/web/src/bootstrap.tsx b/web/src/bootstrap.tsx new file mode 100644 index 000000000..a4b8a8705 --- /dev/null +++ b/web/src/bootstrap.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' + +// This flag is used in services/config.ts to customize API path when app is run +// in multiple modes (standalone vs. embedded). +// Also being used in when generating proper URLs inside the app. +window.APP_RUN_IN_STANDALONE_MODE = true + +ReactDOM.render(, document.getElementById('react-root')) diff --git a/web/src/components/ContainerSpinner/ContainerSpinner.module.scss b/web/src/components/ContainerSpinner/ContainerSpinner.module.scss new file mode 100644 index 000000000..c037cbce3 --- /dev/null +++ b/web/src/components/ContainerSpinner/ContainerSpinner.module.scss @@ -0,0 +1,8 @@ +.spinner { + width: 100%; + height: 100%; + + > div { + position: relative !important; + } +} diff --git a/web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts b/web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts new file mode 100644 index 000000000..25367990c --- /dev/null +++ b/web/src/components/ContainerSpinner/ContainerSpinner.module.scss.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +// this is an auto-generated file +declare const styles: { + readonly spinner: string +} +export default styles diff --git a/web/src/components/ContainerSpinner/ContainerSpinner.tsx b/web/src/components/ContainerSpinner/ContainerSpinner.tsx new file mode 100644 index 000000000..a5b1d7f6e --- /dev/null +++ b/web/src/components/ContainerSpinner/ContainerSpinner.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import cx from 'classnames' +import { Container, PageSpinner } from '@harness/uicore' +import css from './ContainerSpinner.module.scss' + +export const ContainerSpinner: React.FC> = ({ className, ...props }) => { + return ( + + + + ) +} diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss new file mode 100644 index 000000000..3f6be286e --- /dev/null +++ b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss @@ -0,0 +1,42 @@ +.status { + --bg-color: var(--grey-350); + white-space: nowrap !important; + font-size: var(--font-size-xsmall) !important; + color: var(--white) !important; + border: none; + background-color: var(--bg-color) !important; + border-radius: var(--spacing-2); + padding: var(--spacing-1) var(--spacing-3) !important; + height: 18px; + line-height: var(--font-size-normal) !important; + font-weight: bold !important; + display: inline-flex !important; + justify-content: center; + align-items: center; + letter-spacing: 0.2px; + + &.danger { + --bg-color: var(--red-600); + } + + &.none { + --bg-color: var(--grey-800); + } + + &.success { + --bg-color: var(--green-600); + } + + &.primary { + --bg-color: var(--primary-7); + } + + &.warning { + --bg-color: var(--warning); + } + + > span { + margin-right: var(--spacing-2) !important; + color: var(--white) !important; + } +} diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts new file mode 100644 index 000000000..dc3c2ec71 --- /dev/null +++ b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +// this is an auto-generated file +declare const styles: { + readonly status: string + readonly danger: string + readonly none: string + readonly success: string + readonly primary: string + readonly warning: string +} +export default styles diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx b/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx new file mode 100644 index 000000000..fec25d7ce --- /dev/null +++ b/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import cx from 'classnames' +import { Intent, IconName, Text } from '@harness/uicore' +import type { IconProps } from '@harness/uicore/dist/icons/Icon' +import css from './EvaluationStatusLabel.module.scss' + +export interface EvaluationStatusProps { + intent: Intent + label: string + icon?: IconName + iconProps?: IconProps + className?: string +} + +export const EvaluationStatusLabel: React.FC = ({ + intent, + icon, + iconProps, + label, + className +}) => { + let _icon: IconName | undefined = icon + + if (!_icon) { + switch (intent) { + case Intent.DANGER: + case Intent.WARNING: + _icon = 'warning-sign' + break + case Intent.SUCCESS: + _icon = 'tick-circle' + break + } + } + + return ( + + {label} + + ) +} diff --git a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss new file mode 100644 index 000000000..bc6af2a68 --- /dev/null +++ b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss @@ -0,0 +1,61 @@ +.main { + max-width: 785px; + margin-bottom: var(--spacing-medium) !important; + + :global(.bp3-input) { + &:disabled { + background-color: var(--grey-100); + } + } + div[class*='collapse'] { + margin-left: 0; + } + + & div[class*='Collapse'] { + border-top: none; + padding: 0; + + // spacing for adjacent collapsible + + div[class*='Collapse'][class*='main'] { + margin-top: var(--spacing-medium); + } + + div[class*='CollapseHeader'] { + padding-top: 0 !important; + } + + span[data-icon='main-chevron-down'] { + color: var(--primary-7); + } + } + + textarea { + min-height: 50px; + resize: vertical; + } + :global { + .TextInput--main { + margin: 0; + .bp3-form-group { + margin: 0; + } + margin-bottom: var(--spacing-medium); + } + } +} + +.editOpen { + cursor: pointer; + margin-left: 8px !important; + position: relative; + top: -2px; + + &:hover { + color: var(--primary-7) !important; + } +} +.descriptionLabel { + margin-bottom: var(--spacing-xsmall) !important; + display: flex !important; + align-items: center; +} diff --git a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss.d.ts b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss.d.ts new file mode 100644 index 000000000..506953412 --- /dev/null +++ b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.module.scss.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +// this is an auto-generated file +declare const styles: { + readonly main: string + readonly editOpen: string + readonly descriptionLabel: string +} +export default styles diff --git a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx new file mode 100644 index 000000000..2c07071bb --- /dev/null +++ b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx @@ -0,0 +1,223 @@ +import React, { useState } from 'react' +import { Container, FormInput, Icon, Label, DataTooltipInterface, HarnessDocTooltip } from '@harness/uicore' +import type { InputWithIdentifierProps } from '@harness/uicore/dist/components/InputWithIdentifier/InputWithIdentifier' +import { isEmpty } from 'lodash-es' +import { Classes, IInputGroupProps, ITagInputProps } from '@blueprintjs/core' +import cx from 'classnames' +import type { FormikProps } from 'formik' +import { useStrings } from 'framework/strings' +import type { + DescriptionComponentProps, + DescriptionProps, + NameIdDescriptionProps, + NameIdDescriptionTagsDeprecatedProps, + TagsComponentProps, + TagsDeprecatedComponentProps +} from './NameIdDescriptionTagsConstants' +import css from './NameIdDescriptionTags.module.scss' + +export interface NameIdDescriptionTagsProps { + identifierProps?: Omit + inputGroupProps?: IInputGroupProps + descriptionProps?: DescriptionProps + tagsProps?: Partial & { + isOption?: boolean + } + formikProps: FormikProps + className?: string + tooltipProps?: DataTooltipInterface +} + +interface NameIdProps { + nameLabel?: string // Strong default preference for "Name" vs. Contextual Name (e.g. "Service Name") unless approved otherwise + namePlaceholder?: string + identifierProps?: Omit + inputGroupProps?: IInputGroupProps + dataTooltipId?: string +} + +export const NameId = (props: NameIdProps): JSX.Element => { + const { getString } = useStrings() + const { identifierProps, nameLabel = getString('common.name'), inputGroupProps = {} } = props + const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps } + return ( + + ) +} + +export const Description = (props: DescriptionComponentProps): JSX.Element => { + const { descriptionProps = {}, hasValue, disabled = false } = props + const { isOptional = true, ...restDescriptionProps } = descriptionProps + const { getString } = useStrings() + const [isDescriptionOpen, setDescriptionOpen] = useState(hasValue || false) + const [isDescriptionFocus, setDescriptionFocus] = useState(false) + + return ( + + + {isDescriptionOpen && ( + + )} + + ) +} + +export const Tags = (props: TagsComponentProps): JSX.Element => { + const { tagsProps, hasValue, isOptional = true } = props + const { getString } = useStrings() + const [isTagsOpen, setTagsOpen] = useState(hasValue || false) + + return ( + + + {isTagsOpen && } + + ) +} + +function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element { + const { hasValue } = props + const { getString } = useStrings() + const [isTagsOpen, setTagsOpen] = useState(hasValue || false) + + return ( + + + {isTagsOpen && ( + (typeof name === 'string' ? name : '')} + itemFromNewTag={newTag => newTag} + items={[]} + tagInputProps={{ + noInputBorder: true, + openOnKeyDown: false, + showAddTagButton: true, + showClearAllButton: true, + allowNewTag: true + }} + /> + )} + + ) +} + +export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element { + const { getString } = useStrings() + const { + className, + identifierProps, + descriptionProps, + tagsProps, + formikProps, + inputGroupProps = {}, + tooltipProps + } = props + const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps } + return ( + + + + + + ) +} + +// Requires verification with existing tags +export function NameIdDescriptionTagsDeprecated(props: NameIdDescriptionTagsDeprecatedProps): JSX.Element { + const { className, identifierProps, descriptionProps, formikProps } = props + return ( + + + + + + ) +} + +export function NameIdDescription(props: NameIdDescriptionProps): JSX.Element { + const { getString } = useStrings() + const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {} } = props + const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps } + + return ( + + + + + ) +} + +export function DescriptionTags(props: Omit): JSX.Element { + const { className, descriptionProps, tagsProps, formikProps } = props + return ( + + + + + ) +} diff --git a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTagsConstants.ts b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTagsConstants.ts new file mode 100644 index 000000000..2ab0a40bf --- /dev/null +++ b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTagsConstants.ts @@ -0,0 +1,47 @@ +import type { TagInputProps } from '@harness/uicore' +import type { ITagInputProps, IInputGroupProps } from '@blueprintjs/core' +import type { InputWithIdentifierProps } from '@harness/uicore/dist/components/InputWithIdentifier/InputWithIdentifier' +import type { FormikProps } from 'formik' + +export interface DescriptionProps { + placeholder?: string + isOptional?: boolean + disabled?: boolean +} +export interface DescriptionComponentProps { + descriptionProps?: DescriptionProps + hasValue?: boolean + disabled?: boolean + dataTooltipId?: string +} + +export interface TagsProps { + className?: string +} + +export interface TagsComponentProps { + tagsProps?: Partial + hasValue?: boolean + isOptional?: boolean + dataTooltipId?: string +} + +export interface TagsDeprecatedComponentProps { + hasValue?: boolean +} + +export interface NameIdDescriptionTagsDeprecatedProps { + identifierProps?: Omit + descriptionProps?: DescriptionProps + tagInputProps?: TagInputProps + formikProps: FormikProps + className?: string +} + +export interface NameIdDescriptionProps { + identifierProps?: Omit + inputGroupProps?: IInputGroupProps + descriptionProps?: DescriptionProps + className?: string + formikProps: Omit, 'tags'> +} diff --git a/web/src/components/OptionsMenuButton/OptionsMenuButton.tsx b/web/src/components/OptionsMenuButton/OptionsMenuButton.tsx new file mode 100644 index 000000000..0113acfc0 --- /dev/null +++ b/web/src/components/OptionsMenuButton/OptionsMenuButton.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { Classes, Menu } from '@blueprintjs/core' +import { Button, ButtonProps } from '@harness/uicore' +import type { PopoverProps } from '@harness/uicore/dist/components/Popover/Popover' + +export const MenuDivider = '-' as const + +export interface OptionsMenuButtonProps extends ButtonProps { + items: Array | '-'> +} + +export const OptionsMenuButton: React.FC = ({ items, ...props }) => { + return ( + + + + + ) + + return ( + + + refetch()} + error={(error?.data as Error)?.message || error?.message}> + + + {title} + + {editDetails ? ( + editForm + ) : ( + <> + + + {getString('common.name')} + + {name} + + + {getString('common.description')} + {desc} + + + )} + {!editDetails && ( + + + + {showToken && } + + + + + ) +} diff --git a/web/src/components/SideNav/SideNav.module.scss b/web/src/components/SideNav/SideNav.module.scss new file mode 100644 index 000000000..103330ea6 --- /dev/null +++ b/web/src/components/SideNav/SideNav.module.scss @@ -0,0 +1,40 @@ +.root { + display: flex; +} + +.sideNav { + width: 184px !important; + height: 100%; + display: flex; + flex-direction: column; + position: relative; + background: #07182b !important; +} + +.link { + display: block; + margin-left: var(--spacing-medium); + padding: var(--spacing-small) var(--spacing-medium); + opacity: 0.8; + z-index: 1; + + &:hover { + text-decoration: none; + opacity: 1; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + background-color: rgba(2, 120, 213, 0.5); + } + + &.selected { + background-color: rgba(2, 120, 213, 0.8); + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + opacity: 1; + } + + .text { + color: var(--white) !important; + font-size: 13px !important; + } +} diff --git a/web/src/components/SideNav/SideNav.module.scss.d.ts b/web/src/components/SideNav/SideNav.module.scss.d.ts new file mode 100644 index 000000000..dba554547 --- /dev/null +++ b/web/src/components/SideNav/SideNav.module.scss.d.ts @@ -0,0 +1,16 @@ +/* eslint-disable */ +/** + * Copyright 2021 Harness Inc. All rights reserved. + * Use of this source code is governed by the PolyForm Shield 1.0.0 license + * that can be found in the licenses directory at the root of this repository, also available at + * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. + **/ +// this is an auto-generated file, do not update this manually +declare const styles: { + readonly link: string + readonly root: string + readonly selected: string + readonly sideNav: string + readonly text: string +} +export default styles diff --git a/web/src/components/SideNav/SideNav.tsx b/web/src/components/SideNav/SideNav.tsx new file mode 100644 index 000000000..bd8a30b64 --- /dev/null +++ b/web/src/components/SideNav/SideNav.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import cx from 'classnames' +import { NavLink as Link, NavLinkProps } from 'react-router-dom' +import { Container, Text, Layout, IconName } from '@harness/uicore' +import { useAPIToken } from 'hooks/useAPIToken' +import { useStrings } from 'framework/strings' +import routes from 'RouteDefinitions' +import css from './SideNav.module.scss' + +interface SidebarLinkProps extends NavLinkProps { + label: string + icon?: IconName + className?: string +} + +const SidebarLink: React.FC = ({ label, icon, className, ...others }) => ( + + + {label} + + +) + +export const SideNav: React.FC = ({ children }) => { + const { getString } = useStrings() + const [, setToken] = useAPIToken() + + return ( + + + + + setToken('')} icon="log-out" label={getString('logout')} to={routes.toLogin()} /> + + {children} + + ) +} diff --git a/web/src/components/Table/Table.module.scss b/web/src/components/Table/Table.module.scss new file mode 100644 index 000000000..5092eb2b1 --- /dev/null +++ b/web/src/components/Table/Table.module.scss @@ -0,0 +1,11 @@ +.table { + padding-bottom: 0; +} + +.layout { + justify-content: flex-end; +} + +.verticalCenter { + justify-content: center; +} diff --git a/web/src/components/Table/Table.module.scss.d.ts b/web/src/components/Table/Table.module.scss.d.ts new file mode 100644 index 000000000..b8ffd3c7e --- /dev/null +++ b/web/src/components/Table/Table.module.scss.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/** + * Copyright 2021 Harness Inc. All rights reserved. + * Use of this source code is governed by the PolyForm Shield 1.0.0 license + * that can be found in the licenses directory at the root of this repository, also available at + * https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt. + **/ +// this is an auto-generated file, do not update this manually +declare const styles: { + readonly layout: string + readonly table: string + readonly verticalCenter: string +} +export default styles diff --git a/web/src/components/Table/Table.tsx b/web/src/components/Table/Table.tsx new file mode 100644 index 000000000..abbff4b81 --- /dev/null +++ b/web/src/components/Table/Table.tsx @@ -0,0 +1,172 @@ +import React, { useMemo, useState } from 'react' +import moment from 'moment' +import { + Text, + Layout, + Color, + TableV2, + Button, + ButtonVariation, + useConfirmationDialog, + useToaster +} from '@harness/uicore' +import type { CellProps, Renderer, Column } from 'react-table' +import { Menu, Position, Intent, Popover } from '@blueprintjs/core' +import { useStrings } from 'framework/strings' +import type { Pipeline } from 'services/pm' + +import styles from './Table.module.scss' + +interface TableProps { + data: Pipeline[] | null + refetch: () => Promise + onDelete: (value: string) => Promise + onSettingsClick: (slug: string) => void + onRowClick: (slug: string) => void +} + +type CustomColumn> = Column & { + refetch?: () => Promise +} + +const Table: React.FC = ({ data, refetch, onRowClick, onDelete, onSettingsClick }) => { + const RenderColumn: Renderer> = ({ + cell: { + column: { Header }, + row: { values } + } + }) => { + let text + switch (Header) { + case 'ID': + text = values.id + break + case 'Name': + text = values.name + break + case 'Description': + text = values.desc + break + case 'Slug': + text = values.slug + break + case 'Created': + text = moment(values.created).format('MM/DD/YYYY hh:mm:ss a') + break + } + return ( + onRowClick(values.slug)} + spacing="small" + flex={{ alignItems: 'center', justifyContent: 'flex-start' }} + style={{ cursor: 'pointer' }}> + + + + {text} + + + + + ) + } + + const RenderColumnMenu: Renderer> = ({ row: { values } }) => { + const { showSuccess, showError } = useToaster() + const { getString } = useStrings() + const [menuOpen, setMenuOpen] = useState(false) + const { openDialog } = useConfirmationDialog({ + titleText: getString('common.delete'), + contentText: Are you sure you want to delete this?, + confirmButtonText: getString('common.delete'), + cancelButtonText: getString('common.cancel'), + intent: Intent.DANGER, + buttonIntent: Intent.DANGER, + onCloseDialog: async (isConfirmed: boolean) => { + if (isConfirmed) { + try { + await onDelete(values.slug) + showSuccess(getString('common.itemDeleted')) + refetch() + } catch (err) { + showError(`Error: ${err}`) + console.error({ err }) + } + } + } + }) + + return ( + + setMenuOpen(nextOpenState)} + position={Position.BOTTOM_RIGHT} + content={ + + + onSettingsClick(values.slug)} /> + + }> + + + + + + )) + + return ( + + + +