From de36745718a3bb765c3024f42f4b316b57234bcc Mon Sep 17 00:00:00 2001 From: Yaroslav Svitlytskyi <53532703+YarikRevich@users.noreply.github.com> Date: Sun, 12 May 2024 16:32:53 +0200 Subject: [PATCH] Feature: implement basic structure of the project (#13) * feature: added basic project structure * fix: fixed bugs * feature: implemented secrets validation * feature: improved RepoAchiever Cluster module structure * fix: fixed bugs * fix: fixed bugs * fix: added statement closure after execution * fix: fixed bugs * fix: fixed bugs * feature: added diagnostics template processing * fix: fixed bugs * feature: added Grafana deployment * feature: added metrics server * fix: fixed bugs * fix: fixed bugs * feature: added cluster topology withdrawal logic * fix: fixed bugs * feature: added logs during RepoAchiever Cluster operations * fix: fixed bugs * fix: fixed bugs * fix: fixed bugs * fix: fixed bugs * fix: fixed bugs --- .gitignore | 4 + LICENSE | 2 +- Makefile | 135 ++ README.md | 88 + api-server/api-server.iml | 9 + api-server/pom.xml | 317 +++ .../ClusterContextToJsonConverter.java | 31 + ...sToClusterContextCredentialsConverter.java | 18 + ...iderToClusterContextProviderConverter.java | 24 + ...thCheckResponseToReadinessCheckResult.java | 44 + .../dto/ClusterAllocationDto.java | 31 + .../dto/CommandExecutorOutputDto.java | 15 + .../dto/RepositoryContentUnitDto.java | 20 + .../entity/common/ClusterContextEntity.java | 145 ++ .../entity/common/ConfigEntity.java | 197 ++ .../entity/common/MetadataFileEntity.java | 20 + .../entity/common/PropertiesEntity.java | 166 ++ .../entity/repository/ConfigEntity.java | 22 + .../entity/repository/ContentEntity.java | 20 + .../entity/repository/ProviderEntity.java | 15 + .../entity/repository/SecretEntity.java | 23 + ...rverInstanceIsAlreadyRunningException.java | 21 + .../ClusterApplicationFailureException.java | 21 + .../ClusterApplicationTimeoutException.java | 21 + .../ClusterDeploymentFailureException.java | 21 + .../ClusterDestructionFailureException.java | 21 + ...lusterFullDestructionFailureException.java | 21 + .../ClusterOperationFailureException.java | 21 + .../ClusterRecreationFailureException.java | 21 + ...nhealthyReapplicationFailureException.java | 21 + .../ClusterWithdrawalFailureException.java | 21 + .../exception/CommandExecutorException.java | 17 + ...nicationConfigurationFailureException.java | 21 + .../exception/ConfigValidationException.java | 17 + ...tApplicationRetrievalFailureException.java | 21 + .../ContentFileNotFoundException.java | 21 + .../ContentFileRemovalFailureException.java | 21 + .../ContentFileWriteFailureException.java | 21 + .../CredentialsAreNotValidException.java | 21 + .../CredentialsConversionException.java | 17 + .../CredentialsFieldIsNotValidException.java | 21 + ...icsTemplateProcessingFailureException.java | 21 + .../DockerInspectRemovalFailureException.java | 21 + .../DockerIsNotAvailableException.java | 21 + .../DockerNetworkCreateFailureException.java | 21 + .../DockerNetworkRemoveFailureException.java | 21 + .../LocationsFieldIsNotValidException.java | 21 + .../MetadataFileNotFoundException.java | 21 + .../MetadataFileWriteFailureException.java | 21 + ...odeExporterDeploymentFailureException.java | 21 + .../PrometheusDeploymentFailureException.java | 21 + .../exception/QueryEmptyResultException.java | 21 + .../QueryExecutionFailureException.java | 21 + ...oryContentApplicationFailureException.java | 21 + ...oryContentDestructionFailureException.java | 21 + .../RepositoryOperationFailureException.java | 21 + .../TelemetryOperationFailureException.java | 21 + ...tentDirectoryCreationFailureException.java | 21 + ...UnitDirectoryCreationFailureException.java | 21 + ...rkspaceUnitDirectoryNotFoundException.java | 21 + ...orkspaceUnitDirectoryPresentException.java | 23 + ...eUnitDirectoryRemovalFailureException.java | 21 + .../repoachiever/logging/FatalAppender.java | 36 + ...sterApplicationFailureExceptionMapper.java | 20 + ...usterWithdrawalFailureExceptionMapper.java | 20 + ...CredentialsAreNotValidExceptionMapper.java | 18 + ...entialsFieldIsNotValidExceptionMapper.java | 19 + ...cationsFieldIsNotValidExceptionMapper.java | 18 + ...tentApplicationFailureExceptionMapper.java | 20 + ...tentDestructionFailureExceptionMapper.java | 20 + ...eUnitDirectoryNotFoundExceptionMapper.java | 18 + .../repository/ConfigRepository.java | 123 ++ .../repository/ContentRepository.java | 149 ++ .../repository/ProviderRepository.java | 153 ++ .../repository/SecretRepository.java | 194 ++ .../common/RepositoryConfigurationHelper.java | 57 + .../executor/RepositoryExecutor.java | 154 ++ .../repository/facade/RepositoryFacade.java | 212 ++ .../resource/ContentResource.java | 128 ++ .../repoachiever/resource/HealthResource.java | 65 + .../repoachiever/resource/InfoResource.java | 56 + .../repoachiever/resource/StateResource.java | 28 + .../common/ResourceConfigurationHelper.java | 37 + .../ApiServerCommunicationResource.java | 56 + .../client/github/IGitHubClientService.java | 19 + .../ISmallRyeHealthCheckClientService.java | 18 + .../service/cluster/ClusterService.java | 174 ++ .../common/ClusterConfigurationHelper.java | 70 + .../service/cluster/facade/ClusterFacade.java | 288 +++ .../ClusterCommunicationResource.java | 142 ++ .../common/ClusterConfigurationHelper.java | 25 + .../deploy/ClusterDeployCommandService.java | 38 + .../destroy/ClusterDestroyCommandService.java | 31 + .../common/CommandConfigurationHelper.java | 71 + ...DockerAvailabilityCheckCommandService.java | 33 + .../DockerInspectRemoveCommandService.java | 32 + .../DockerNetworkCreateCommandService.java | 34 + .../DockerNetworkRemoveCommandService.java | 31 + .../grafana/GrafanaDeployCommandService.java | 38 + .../common/GrafanaConfigurationHelper.java | 57 + .../NodeExporterDeployCommandService.java | 38 + .../NodeExporterConfigurationHelper.java | 60 + .../PrometheusDeployCommandService.java | 56 + .../common/PrometheusConfigurationHelper.java | 86 + .../IApiServerCommunicationService.java | 45 + .../cluster/IClusterCommunicationService.java | 50 + ...municationProviderConfigurationHelper.java | 15 + .../service/config/ConfigService.java | 103 + .../executor/CommandExecutorService.java | 62 + .../health/HealthCheckService.java | 20 + .../readiness/ReadinessCheckService.java | 23 + .../ApiServerCommunicationConfigService.java | 60 + ...lusterHealthCheckCommunicationService.java | 58 + ...terTopologyCommunicationConfigService.java | 65 + .../RegistryCommunicationConfigService.java | 48 + .../diagnostics/DiagnosticsConfigService.java | 335 +++ .../telemetry/TelemetryConfigService.java | 168 ++ .../template/TemplateConfigService.java | 214 ++ .../http/HttpServerConfigService.java | 28 + .../GeneralPropertiesConfigService.java | 17 + .../git/GitPropertiesConfigService.java | 75 + .../integration/state/StateConfigService.java | 66 + .../service/state/StateService.java | 80 + .../service/telemetry/TelemetryService.java | 57 + .../telemetry/binding/TelemetryBinding.java | 44 + .../service/vendor/VendorFacade.java | 28 + .../common/VendorConfigurationHelper.java | 16 + .../git/github/GitGitHubVendorService.java | 33 + .../service/workspace/WorkspaceService.java | 362 ++++ .../workspace/facade/WorkspaceFacade.java | 114 + api-server/src/main/openapi/openapi.yml | 352 ++++ ...lipse.microprofile.config.spi.ConfigSource | 1 + .../src/main/resources/application.properties | 155 ++ .../src/main/resources/liquibase/config.yaml | 104 + .../main/resources/liquibase/data/data.csv | 3 + api-server/src/main/resources/log4j2.xml | 60 + .../core/config/plugins/Log4j2Plugins.dat | Bin 0 -> 81 bytes .../classes/META-INF/panache-archive.marker | 1 + ...lipse.microprofile.config.spi.ConfigSource | 1 + .../target/classes/application.properties | 155 ++ api-server/target/classes/git.properties | 22 + .../target/classes/liquibase/config.yaml | 104 + .../target/classes/liquibase/data/data.csv | 3 + api-server/target/classes/log4j2.xml | 60 + .../failsafe-reports/failsafe-summary.xml | 8 + .../generated-sources/openapi/.dockerignore | 4 + .../openapi/.openapi-generator-ignore | 23 + .../openapi/.openapi-generator/FILES | 35 + .../openapi/.openapi-generator/VERSION | 1 + .../openapi.yml-default.sha256 | 1 + .../generated-sources/openapi/README.md | 15 + .../target/generated-sources/openapi/pom.xml | 137 ++ .../openapi/src/main/docker/Dockerfile.jvm | 34 + .../openapi/src/main/docker/Dockerfile.native | 22 + .../com/repoachiever/RestResourceRoot.java | 5 + .../repoachiever/api/ContentResourceApi.java | 49 + .../repoachiever/api/HealthResourceApi.java | 32 + .../com/repoachiever/api/InfoResourceApi.java | 35 + .../repoachiever/api/StateResourceApi.java | 25 + .../repoachiever/model/ClusterInfoUnit.java | 123 ++ .../model/ContentApplication.java | 145 ++ .../repoachiever/model/ContentCleanup.java | 82 + .../model/ContentRetrievalApplication.java | 105 + .../model/ContentRetrievalResult.java | 99 + .../model/ContentStateApplication.java | 105 + .../model/ContentStateApplicationResult.java | 81 + .../repoachiever/model/ContentWithdrawal.java | 105 + .../model/CredentialsFieldsExternal.java | 82 + .../model/CredentialsFieldsFull.java | 104 + .../model/CredentialsFieldsInternal.java | 80 + .../model/GitGitHubCredentials.java | 81 + .../repoachiever/model/HealthCheckResult.java | 123 ++ .../repoachiever/model/HealthCheckStatus.java | 57 + .../repoachiever/model/HealthCheckUnit.java | 104 + .../java/com/repoachiever/model/Provider.java | 57 + .../model/ReadinessCheckApplication.java | 80 + .../model/ReadinessCheckResult.java | 126 ++ .../model/ReadinessCheckStatus.java | 57 + .../model/ReadinessCheckUnit.java | 104 + .../model/VersionExternalApiInfoResult.java | 103 + .../repoachiever/model/VersionInfoResult.java | 81 + .../src/main/resources/META-INF/openapi.yaml | 465 +++++ .../src/main/resources/application.properties | 5 + .../target/maven-archiver/pom.properties | 4 + .../compile/default-compile/createdFiles.lst | 210 ++ .../compile/default-compile/inputFiles.lst | 151 ++ .../quarkus-app/quarkus-app-dependencies.txt | 293 +++ .../quarkus/quarkus-application.dat | Bin 0 -> 180591 bytes api-server/target/quarkus-artifact.properties | 4 + cli/.DS_Store | Bin 0 -> 6148 bytes cli/cli.iml | 10 + cli/pom.xml | 224 ++ cli/src/.DS_Store | Bin 0 -> 6148 bytes cli/src/main/java/com/repoachiever/App.java | 94 + cli/src/main/java/com/repoachiever/CLI.java | 13 + .../converter/CredentialsConverter.java | 11 + .../dto/ValidationScriptApplicationDto.java | 12 + .../dto/ValidationSecretsApplicationDto.java | 14 + .../dto/VisualizationLabelDto.java | 26 + .../com/repoachiever/entity/ConfigEntity.java | 72 + .../repoachiever/entity/PropertiesEntity.java | 79 + .../exception/ApiServerException.java | 18 + .../ApiServerNotAvailableException.java | 18 + ...CloudCredentialsFileNotFoundException.java | 19 + .../CloudCredentialsValidationException.java | 18 + .../exception/ConfigValidationException.java | 14 + .../ScriptDataFileNotFoundException.java | 18 + .../ScriptDataValidationException.java | 19 + .../exception/VersionMismatchException.java | 21 + .../repoachiever/logging/FatalAppender.java | 48 + .../service/client/IClientCommand.java | 19 + .../command/ApplyClientCommandService.java | 44 + .../command/DestroyClientCommandService.java | 40 + .../HealthCheckClientCommandService.java | 39 + .../command/LogsClientCommandService.java | 42 + .../ReadinessCheckClientCommandService.java | 41 + .../ScriptAcquireClientCommandService.java | 46 + .../SecretsAcquireClientCommandService.java | 66 + .../command/VersionClientCommandService.java | 39 + .../service/command/BaseCommandService.java | 134 ++ .../service/command/common/ICommand.java | 9 + .../start/StartExternalCommandService.java | 26 + .../aws/AWSStartExternalCommandService.java | 120 ++ .../state/StateExternalCommandService.java | 25 + .../aws/AWSStateExternalCommandService.java | 86 + .../stop/StopExternalCommandService.java | 26 + .../aws/AWSStopExternalCommandService.java | 80 + .../VersionExternalCommandService.java | 41 + .../HealthCheckInternalCommandService.java | 34 + .../ReadinessCheckInternalCommandService.java | 28 + ...SReadinessCheckInternalCommandService.java | 74 + .../service/config/ConfigService.java | 89 + .../config/common/ValidConfigService.java | 40 + .../visualization/VisualizationService.java | 50 + .../common/IVisualizationLabel.java | 28 + .../label/StartCommandVisualizationLabel.java | 82 + .../label/StateCommandVisualizationLabel.java | 82 + .../label/StopCommandVisualizationLabel.java | 80 + .../VersionCommandVisualizationLabel.java | 77 + .../state/VisualizationState.java | 28 + cli/src/main/resources/application.properties | 43 + cli/src/main/resources/log4j2.xml | 60 + cluster/pom.xml | 176 ++ .../src/main/java/com/repoachiever/App.java | 37 + .../main/java/com/repoachiever/Cluster.java | 15 + .../converter/CronExpressionConverter.java | 28 + .../dto/CommandExecutorOutputDto.java | 13 + .../com/repoachiever/entity/ConfigEntity.java | 170 ++ .../repoachiever/entity/PropertiesEntity.java | 56 + .../ApiServerOperationFailureException.java | 21 + .../exception/CommandExecutorException.java | 17 + ...nicationConfigurationFailureException.java | 21 + .../exception/ConfigNotGivenException.java | 21 + .../exception/ConfigValidationException.java | 21 + .../exception/CronExpressionException.java | 17 + .../repoachiever/logging/FatalAppender.java | 36 + .../logging/TransferAppender.java | 39 + .../common/LoggingConfigurationHelper.java | 38 + .../ClusterCommunicationResource.java | 56 + .../ApiServerCommunicationResource.java | 128 ++ .../IApiServerCommunicationService.java | 45 + .../cluster/IClusterCommunicationService.java | 50 + ...municationProviderConfigurationHelper.java | 15 + .../service/config/ConfigService.java | 106 + .../executor/CommandExecutorService.java | 59 + .../ClusterCommunicationConfigService.java | 58 + .../logging/state/LoggingStateService.java | 57 + .../transfer/LoggingTransferService.java | 53 + .../service/state/StateService.java | 42 + .../service/waiter/WaiterHelper.java | 17 + .../src/main/resources/application.properties | 12 + cluster/src/main/resources/log4j2.xml | 17 + config/grafana/dashboards/dashboard.yml | 11 + config/grafana/dashboards/diagnostics.tmpl | 1831 +++++++++++++++++ config/grafana/datasources/datasource.tmpl | 19 + config/prometheus/prometheus.tmpl | 18 + docs/detailed-design-raw.md | 76 + docs/detailed-design.png | Bin 0 -> 124433 bytes docs/high-level-design-raw.md | 23 + docs/high-level-design.png | Bin 0 -> 27497 bytes docs/workspace-design.md | 18 + docs/workspace-design.png | Bin 0 -> 12162 bytes exporter/pom.xml | 21 + exporter/src/main/java/org/example/Main.java | 7 + gui/.DS_Store | Bin 0 -> 6148 bytes gui/gui.iml | 8 + gui/pom.xml | 206 ++ gui/src/.DS_Store | Bin 0 -> 6148 bytes gui/src/main/.DS_Store | Bin 0 -> 6148 bytes gui/src/main/java/com/repoachiever/App.java | 81 + gui/src/main/java/com/repoachiever/GUI.java | 11 + .../converter/CredentialsConverter.java | 11 + .../dto/CommandExecutorOutputDto.java | 13 + .../HealthCheckInternalCommandResultDto.java | 13 + ...eadinessCheckInternalCommandResultDto.java | 13 + .../dto/StartExternalCommandResultDto.java | 13 + .../dto/StateExternalCommandResultDto.java | 16 + .../dto/StopExternalCommandResultDto.java | 13 + .../dto/ValidationScriptApplicationDto.java | 12 + .../dto/ValidationSecretsApplicationDto.java | 14 + .../dto/VersionExternalCommandResultDto.java | 15 + .../com/repoachiever/entity/ConfigEntity.java | 66 + .../repoachiever/entity/PropertiesEntity.java | 211 ++ .../exception/ApiServerException.java | 18 + .../ApiServerNotAvailableException.java | 10 + .../ApplicationFontFileNotFoundException.java | 16 + ...ApplicationImageFileNotFoundException.java | 20 + ...CloudCredentialsFileNotFoundException.java | 19 + .../CloudCredentialsValidationException.java | 18 + .../exception/CommandExecutorException.java | 10 + .../ScriptDataValidationException.java | 19 + .../SmartGraphCssFileNotFoundException.java | 13 + ...tGraphPropertiesFileNotFoundException.java | 13 + .../SwapFileCreationFailedException.java | 19 + .../SwapFileDeletionFailedException.java | 19 + .../command/ApplyClientCommandService.java | 52 + .../command/DestroyClientCommandService.java | 47 + .../HealthCheckClientCommandService.java | 47 + .../command/LogsClientCommandService.java | 46 + .../ReadinessCheckClientCommandService.java | 49 + .../ScriptAcquireClientCommandService.java | 54 + .../SecretsAcquireClientCommandService.java | 74 + .../command/VersionClientCommandService.java | 47 + .../service/client/common/IClientCommand.java | 22 + .../client/observer/ResourceObserver.java | 52 + .../service/command/common/ICommand.java | 9 + .../start/StartExternalCommandService.java | 25 + .../aws/AWSStartExternalCommandService.java | 110 + .../state/StateExternalCommandService.java | 25 + .../aws/AWSStateExternalCommandService.java | 72 + .../stop/StopExternalCommandService.java | 25 + .../aws/AWSStopExternalCommandService.java | 75 + .../VersionExternalCommandService.java | 33 + .../HealthCheckInternalCommandService.java | 41 + .../ReadinessCheckInternalCommandService.java | 28 + ...SReadinessCheckInternalCommandService.java | 82 + .../service/config/ConfigService.java | 78 + .../service/element/alert/ErrorAlert.java | 43 + .../element/alert/InformationAlert.java | 43 + .../service/element/button/BasicButton.java | 75 + .../service/element/common/ElementHelper.java | 95 + .../service/element/font/FontLoader.java | 48 + .../element/graph/GraphVisualizer.java | 109 + .../ConnectionStatusImageCollection.java | 26 + .../common/ConnectionStatusImageView.java | 121 ++ .../EditDeploymentConfigurationImageView.java | 92 + .../image/view/common/IconImageView.java | 40 + .../RefreshDeploymentStateImageView.java | 90 + .../view/common/StartDeploymentImageView.java | 90 + .../view/common/StopDeploymentImageView.java | 90 + ...inDeploymentConnectionStatusImageView.java | 15 + .../MainStartConnectionStatusImageView.java | 15 + .../element/layout/common/ContentGrid.java | 47 + .../scene/main/common/MainFooterGrid.java | 55 + .../scene/main/common/MainHeaderGrid.java | 57 + .../scene/main/common/MainMenuButtonBox.java | 103 + .../deployment/MainDeploymentSceneLayout.java | 85 + .../common/MainDeploymentBarGrid.java | 94 + .../common/MainDeploymentContentGrid.java | 67 + .../common/MainDeploymentFooterGrid.java | 17 + .../common/MainDeploymentHeaderGrid.java | 18 + .../common/MainDeploymentMenuButtonBox.java | 27 + .../main/start/MainStartSceneLayout.java | 81 + .../start/common/MainStartFooterGrid.java | 14 + .../start/common/MainStartHeaderGrid.java | 18 + .../start/common/MainStartMenuButtonBox.java | 26 + .../settings/SettingsGeneralSceneLayout.java | 72 + .../common/SettingsMenuButtonBox.java | 56 + .../service/element/list/ListVisualizer.java | 114 + .../element/list/cell/ListVisualizerCell.java | 27 + .../service/element/menu/TabMenuBar.java | 30 + .../element/observer/ElementObserver.java | 55 + .../progressbar/common/CircleProgressBar.java | 66 + .../MainDeploymentCircleProgressBar.java | 15 + .../start/MainStartCircleProgressBar.java | 15 + .../settings/SettingsCircleProgressBar.java | 15 + .../main/deployment/MainDeploymentScene.java | 42 + .../scene/main/start/MainStartScene.java | 42 + .../scene/settings/SettingsGeneralScene.java | 41 + .../service/element/stage/MainStage.java | 102 + .../service/element/stage/SettingsStage.java | 76 + .../element/storage/ElementStorage.java | 70 + .../element/text/common/BuildVersionText.java | 46 + .../service/element/text/common/IElement.java | 13 + .../text/common/IElementActualizable.java | 10 + .../text/common/IElementResizable.java | 10 + .../text/common/LandingAnnouncementText.java | 63 + .../MainDeploymentBuildVersionText.java | 15 + .../main/start/MainStartBuildVersionText.java | 15 + .../service/event/common/EventType.java | 14 + .../service/event/common/IEvent.java | 11 + .../event/payload/ConfigReloadEvent.java | 19 + .../event/payload/ConnectionStatusEvent.java | 25 + .../DeploymentStateRetrievalEvent.java | 21 + .../event/payload/EditorOpenWindowEvent.java | 19 + .../payload/MainWindowHeightUpdateEvent.java | 25 + .../payload/MainWindowWidthUpdateEvent.java | 25 + .../event/payload/StartDeploymentEvent.java | 21 + .../event/payload/StopDeploymentEvent.java | 21 + .../payload/SwapFileOpenWindowEvent.java | 27 + .../service/event/state/LocalState.java | 443 ++++ .../command/StartApiServerCommandService.java | 36 + .../OpenConfigEditorCommandService.java | 33 + .../OpenSwapFileEditorCommandService.java | 31 + .../hand/executor/CommandExecutorService.java | 70 + .../service/scheduler/SchedulerHelper.java | 59 + .../scheduler/storage/SchedulerStorage.java | 27 + .../service/swap/SwapService.java | 59 + gui/src/main/resources/.DS_Store | Bin 0 -> 6148 bytes gui/src/main/resources/application.properties | 124 ++ gui/src/main/resources/banner.txt | 8 + .../resources/font/e-Ukraine-UltraLight.otf | Bin 0 -> 70316 bytes gui/src/main/resources/image/.DS_Store | Bin 0 -> 6148 bytes gui/src/main/resources/image/arrow.png | Bin 0 -> 662 bytes gui/src/main/resources/image/edit.png | Bin 0 -> 411 bytes gui/src/main/resources/image/icon.png | Bin 0 -> 466438 bytes gui/src/main/resources/image/refresh.png | Bin 0 -> 902 bytes gui/src/main/resources/image/start.png | Bin 0 -> 517 bytes gui/src/main/resources/image/stop.png | Bin 0 -> 179 bytes gui/src/main/resources/smartgraph.css | 48 + gui/src/main/resources/smartgraph.properties | 17 + lib/.DS_Store | Bin 0 -> 6148 bytes pom.xml | 602 ++++++ samples/config/api-server/api-server.yaml | 56 + samples/config/client/user.yaml | 26 + worker/pom.xml | 145 ++ .../src/main/java/com/repoachiever/Agent.java | 12 + .../src/main/java/com/repoachiever/App.java | 42 + .../converter/CronExpressionConverter.java | 25 + .../dto/CommandExecutorOutputDto.java | 13 + .../com/repoachiever/entity/ConfigEntity.java | 23 + .../repoachiever/entity/PropertiesEntity.java | 19 + .../exception/CommandExecutorException.java | 14 + .../exception/ConfigValidationException.java | 14 + .../exception/CronExpressionException.java | 14 + .../exception/KafkaProducerSendException.java | 18 + .../repoachiever/logging/FatalAppender.java | 40 + .../service/config/ConfigService.java | 71 + .../config/common/ValidConfigService.java | 40 + .../listener/ClusterListenerService.java | 8 + .../service/scheduler/SchedulerService.java | 118 ++ .../scheduler/command/ExecCommandService.java | 30 + .../executor/CommandExecutorService.java | 59 + .../service/vendor/VendorFacade.java | 4 + .../git/github/GitGitHubVendorService.java | 4 + .../git/local/GitLocalVendorService.java | 4 + .../service/waiter/WaiterHelper.java | 17 + .../src/main/resources/application.properties | 7 + worker/src/main/resources/log4j2.xml | 60 + 449 files changed, 25300 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 api-server/api-server.iml create mode 100644 api-server/pom.xml create mode 100644 api-server/src/main/java/com/repoachiever/converter/ClusterContextToJsonConverter.java create mode 100644 api-server/src/main/java/com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.java create mode 100644 api-server/src/main/java/com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.java create mode 100644 api-server/src/main/java/com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.java create mode 100644 api-server/src/main/java/com/repoachiever/dto/ClusterAllocationDto.java create mode 100644 api-server/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java create mode 100644 api-server/src/main/java/com/repoachiever/dto/RepositoryContentUnitDto.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/common/ClusterContextEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/common/ConfigEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/common/MetadataFileEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/common/PropertiesEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/repository/ConfigEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/repository/ContentEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/repository/ProviderEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/entity/repository/SecretEntity.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterApplicationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterApplicationTimeoutException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterDeploymentFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterDestructionFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterFullDestructionFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterOperationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterRecreationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ClusterWithdrawalFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/CommandExecutorException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ConfigValidationException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ContentApplicationRetrievalFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ContentFileNotFoundException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ContentFileRemovalFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/ContentFileWriteFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/CredentialsAreNotValidException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/CredentialsConversionException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/CredentialsFieldIsNotValidException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/DockerInspectRemovalFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/DockerIsNotAvailableException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/DockerNetworkCreateFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/DockerNetworkRemoveFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/LocationsFieldIsNotValidException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/MetadataFileNotFoundException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/MetadataFileWriteFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/NodeExporterDeploymentFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/PrometheusDeploymentFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/QueryEmptyResultException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/QueryExecutionFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/RepositoryContentApplicationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/RepositoryContentDestructionFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/RepositoryOperationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/TelemetryOperationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.java create mode 100644 api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.java create mode 100644 api-server/src/main/java/com/repoachiever/logging/FatalAppender.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/ConfigRepository.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/ContentRepository.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/ProviderRepository.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/SecretRepository.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/common/RepositoryConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/executor/RepositoryExecutor.java create mode 100644 api-server/src/main/java/com/repoachiever/repository/facade/RepositoryFacade.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/ContentResource.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/HealthResource.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/InfoResource.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/StateResource.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/common/ResourceConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/resource/communication/ApiServerCommunicationResource.java create mode 100644 api-server/src/main/java/com/repoachiever/service/client/github/IGitHubClientService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/cluster/ClusterService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/cluster/common/ClusterConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/cluster/facade/ClusterFacade.java create mode 100644 api-server/src/main/java/com/repoachiever/service/cluster/resource/ClusterCommunicationResource.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/common/CommandConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/grafana/GrafanaDeployCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/config/ConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/healthcheck/health/HealthCheckService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/http/HttpServerConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/integration/state/StateConfigService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/state/StateService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/telemetry/TelemetryService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/telemetry/binding/TelemetryBinding.java create mode 100644 api-server/src/main/java/com/repoachiever/service/vendor/VendorFacade.java create mode 100644 api-server/src/main/java/com/repoachiever/service/vendor/common/VendorConfigurationHelper.java create mode 100644 api-server/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/workspace/WorkspaceService.java create mode 100644 api-server/src/main/java/com/repoachiever/service/workspace/facade/WorkspaceFacade.java create mode 100644 api-server/src/main/openapi/openapi.yml create mode 100644 api-server/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource create mode 100644 api-server/src/main/resources/application.properties create mode 100644 api-server/src/main/resources/liquibase/config.yaml create mode 100644 api-server/src/main/resources/liquibase/data/data.csv create mode 100644 api-server/src/main/resources/log4j2.xml create mode 100644 api-server/target/classes/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat create mode 100644 api-server/target/classes/META-INF/panache-archive.marker create mode 100644 api-server/target/classes/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource create mode 100644 api-server/target/classes/application.properties create mode 100644 api-server/target/classes/git.properties create mode 100644 api-server/target/classes/liquibase/config.yaml create mode 100644 api-server/target/classes/liquibase/data/data.csv create mode 100644 api-server/target/classes/log4j2.xml create mode 100644 api-server/target/failsafe-reports/failsafe-summary.xml create mode 100644 api-server/target/generated-sources/openapi/.dockerignore create mode 100644 api-server/target/generated-sources/openapi/.openapi-generator-ignore create mode 100644 api-server/target/generated-sources/openapi/.openapi-generator/FILES create mode 100644 api-server/target/generated-sources/openapi/.openapi-generator/VERSION create mode 100644 api-server/target/generated-sources/openapi/.openapi-generator/openapi.yml-default.sha256 create mode 100644 api-server/target/generated-sources/openapi/README.md create mode 100644 api-server/target/generated-sources/openapi/pom.xml create mode 100644 api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.jvm create mode 100644 api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.native create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/RestResourceRoot.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/ContentResourceApi.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/HealthResourceApi.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/InfoResourceApi.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/StateResourceApi.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ClusterInfoUnit.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentApplication.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentCleanup.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalApplication.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplication.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplicationResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentWithdrawal.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsFull.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/GitGitHubCredentials.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckStatus.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckUnit.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/Provider.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckApplication.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckStatus.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckUnit.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionInfoResult.java create mode 100644 api-server/target/generated-sources/openapi/src/main/resources/META-INF/openapi.yaml create mode 100644 api-server/target/generated-sources/openapi/src/main/resources/application.properties create mode 100644 api-server/target/maven-archiver/pom.properties create mode 100644 api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 api-server/target/quarkus-app/quarkus-app-dependencies.txt create mode 100644 api-server/target/quarkus-app/quarkus/quarkus-application.dat create mode 100644 api-server/target/quarkus-artifact.properties create mode 100644 cli/.DS_Store create mode 100644 cli/cli.iml create mode 100644 cli/pom.xml create mode 100644 cli/src/.DS_Store create mode 100644 cli/src/main/java/com/repoachiever/App.java create mode 100644 cli/src/main/java/com/repoachiever/CLI.java create mode 100644 cli/src/main/java/com/repoachiever/converter/CredentialsConverter.java create mode 100644 cli/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java create mode 100644 cli/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java create mode 100644 cli/src/main/java/com/repoachiever/dto/VisualizationLabelDto.java create mode 100644 cli/src/main/java/com/repoachiever/entity/ConfigEntity.java create mode 100644 cli/src/main/java/com/repoachiever/entity/PropertiesEntity.java create mode 100644 cli/src/main/java/com/repoachiever/exception/ApiServerException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/ConfigValidationException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/ScriptDataFileNotFoundException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java create mode 100644 cli/src/main/java/com/repoachiever/exception/VersionMismatchException.java create mode 100644 cli/src/main/java/com/repoachiever/logging/FatalAppender.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/IClientCommand.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/BaseCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/common/ICommand.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java create mode 100644 cli/src/main/java/com/repoachiever/service/config/ConfigService.java create mode 100644 cli/src/main/java/com/repoachiever/service/config/common/ValidConfigService.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/VisualizationService.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/common/IVisualizationLabel.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/common/label/StartCommandVisualizationLabel.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/common/label/StateCommandVisualizationLabel.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/common/label/StopCommandVisualizationLabel.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/common/label/VersionCommandVisualizationLabel.java create mode 100644 cli/src/main/java/com/repoachiever/service/visualization/state/VisualizationState.java create mode 100644 cli/src/main/resources/application.properties create mode 100644 cli/src/main/resources/log4j2.xml create mode 100644 cluster/pom.xml create mode 100644 cluster/src/main/java/com/repoachiever/App.java create mode 100644 cluster/src/main/java/com/repoachiever/Cluster.java create mode 100644 cluster/src/main/java/com/repoachiever/converter/CronExpressionConverter.java create mode 100644 cluster/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java create mode 100644 cluster/src/main/java/com/repoachiever/entity/ConfigEntity.java create mode 100644 cluster/src/main/java/com/repoachiever/entity/PropertiesEntity.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/ApiServerOperationFailureException.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/CommandExecutorException.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/ConfigNotGivenException.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/ConfigValidationException.java create mode 100644 cluster/src/main/java/com/repoachiever/exception/CronExpressionException.java create mode 100644 cluster/src/main/java/com/repoachiever/logging/FatalAppender.java create mode 100644 cluster/src/main/java/com/repoachiever/logging/TransferAppender.java create mode 100644 cluster/src/main/java/com/repoachiever/logging/common/LoggingConfigurationHelper.java create mode 100644 cluster/src/main/java/com/repoachiever/resource/communication/ClusterCommunicationResource.java create mode 100644 cluster/src/main/java/com/repoachiever/service/apiserver/resource/ApiServerCommunicationResource.java create mode 100644 cluster/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java create mode 100644 cluster/src/main/java/com/repoachiever/service/config/ConfigService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/integration/communication/cluster/ClusterCommunicationConfigService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/integration/logging/state/LoggingStateService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/integration/logging/transfer/LoggingTransferService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/state/StateService.java create mode 100644 cluster/src/main/java/com/repoachiever/service/waiter/WaiterHelper.java create mode 100644 cluster/src/main/resources/application.properties create mode 100644 cluster/src/main/resources/log4j2.xml create mode 100644 config/grafana/dashboards/dashboard.yml create mode 100644 config/grafana/dashboards/diagnostics.tmpl create mode 100644 config/grafana/datasources/datasource.tmpl create mode 100644 config/prometheus/prometheus.tmpl create mode 100644 docs/detailed-design-raw.md create mode 100644 docs/detailed-design.png create mode 100644 docs/high-level-design-raw.md create mode 100644 docs/high-level-design.png create mode 100644 docs/workspace-design.md create mode 100644 docs/workspace-design.png create mode 100644 exporter/pom.xml create mode 100644 exporter/src/main/java/org/example/Main.java create mode 100644 gui/.DS_Store create mode 100644 gui/gui.iml create mode 100644 gui/pom.xml create mode 100644 gui/src/.DS_Store create mode 100644 gui/src/main/.DS_Store create mode 100644 gui/src/main/java/com/repoachiever/App.java create mode 100644 gui/src/main/java/com/repoachiever/GUI.java create mode 100644 gui/src/main/java/com/repoachiever/converter/CredentialsConverter.java create mode 100644 gui/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/HealthCheckInternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/ReadinessCheckInternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/StartExternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/StateExternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/StopExternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java create mode 100644 gui/src/main/java/com/repoachiever/dto/VersionExternalCommandResultDto.java create mode 100644 gui/src/main/java/com/repoachiever/entity/ConfigEntity.java create mode 100644 gui/src/main/java/com/repoachiever/entity/PropertiesEntity.java create mode 100644 gui/src/main/java/com/repoachiever/exception/ApiServerException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/ApplicationFontFileNotFoundException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/ApplicationImageFileNotFoundException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/CommandExecutorException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/SmartGraphCssFileNotFoundException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/SmartGraphPropertiesFileNotFoundException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/SwapFileCreationFailedException.java create mode 100644 gui/src/main/java/com/repoachiever/exception/SwapFileDeletionFailedException.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/common/IClientCommand.java create mode 100644 gui/src/main/java/com/repoachiever/service/client/observer/ResourceObserver.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/common/ICommand.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/config/ConfigService.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/alert/ErrorAlert.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/alert/InformationAlert.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/button/BasicButton.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/common/ElementHelper.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/font/FontLoader.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/graph/GraphVisualizer.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/collection/ConnectionStatusImageCollection.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/ConnectionStatusImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/EditDeploymentConfigurationImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/IconImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/RefreshDeploymentStateImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/StartDeploymentImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/common/StopDeploymentImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/main/deployment/MainDeploymentConnectionStatusImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/image/view/main/start/MainStartConnectionStatusImageView.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/common/ContentGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/common/MainFooterGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/common/MainHeaderGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/common/MainMenuButtonBox.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/MainDeploymentSceneLayout.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/common/MainDeploymentBarGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/common/MainDeploymentContentGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/common/MainDeploymentFooterGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/common/MainDeploymentHeaderGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/deployment/common/MainDeploymentMenuButtonBox.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/start/MainStartSceneLayout.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/start/common/MainStartFooterGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/start/common/MainStartHeaderGrid.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/main/start/common/MainStartMenuButtonBox.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/settings/SettingsGeneralSceneLayout.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/layout/scene/settings/common/SettingsMenuButtonBox.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/list/ListVisualizer.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/list/cell/ListVisualizerCell.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/menu/TabMenuBar.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/observer/ElementObserver.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/progressbar/common/CircleProgressBar.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/progressbar/main/deployment/MainDeploymentCircleProgressBar.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/progressbar/main/start/MainStartCircleProgressBar.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/progressbar/settings/SettingsCircleProgressBar.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/scene/main/deployment/MainDeploymentScene.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/scene/main/start/MainStartScene.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/scene/settings/SettingsGeneralScene.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/stage/MainStage.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/stage/SettingsStage.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/storage/ElementStorage.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/common/BuildVersionText.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/common/IElement.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/common/IElementActualizable.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/common/IElementResizable.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/common/LandingAnnouncementText.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/main/deployment/MainDeploymentBuildVersionText.java create mode 100644 gui/src/main/java/com/repoachiever/service/element/text/main/start/MainStartBuildVersionText.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/common/EventType.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/common/IEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/ConfigReloadEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/ConnectionStatusEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/DeploymentStateRetrievalEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/EditorOpenWindowEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/MainWindowHeightUpdateEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/MainWindowWidthUpdateEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/StartDeploymentEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/StopDeploymentEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/payload/SwapFileOpenWindowEvent.java create mode 100644 gui/src/main/java/com/repoachiever/service/event/state/LocalState.java create mode 100644 gui/src/main/java/com/repoachiever/service/hand/apiserver/command/StartApiServerCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/hand/config/command/OpenConfigEditorCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/hand/config/command/OpenSwapFileEditorCommandService.java create mode 100644 gui/src/main/java/com/repoachiever/service/hand/executor/CommandExecutorService.java create mode 100644 gui/src/main/java/com/repoachiever/service/scheduler/SchedulerHelper.java create mode 100644 gui/src/main/java/com/repoachiever/service/scheduler/storage/SchedulerStorage.java create mode 100644 gui/src/main/java/com/repoachiever/service/swap/SwapService.java create mode 100644 gui/src/main/resources/.DS_Store create mode 100644 gui/src/main/resources/application.properties create mode 100644 gui/src/main/resources/banner.txt create mode 100644 gui/src/main/resources/font/e-Ukraine-UltraLight.otf create mode 100644 gui/src/main/resources/image/.DS_Store create mode 100644 gui/src/main/resources/image/arrow.png create mode 100644 gui/src/main/resources/image/edit.png create mode 100644 gui/src/main/resources/image/icon.png create mode 100644 gui/src/main/resources/image/refresh.png create mode 100644 gui/src/main/resources/image/start.png create mode 100644 gui/src/main/resources/image/stop.png create mode 100644 gui/src/main/resources/smartgraph.css create mode 100644 gui/src/main/resources/smartgraph.properties create mode 100644 lib/.DS_Store create mode 100644 pom.xml create mode 100644 samples/config/api-server/api-server.yaml create mode 100644 samples/config/client/user.yaml create mode 100644 worker/pom.xml create mode 100644 worker/src/main/java/com/repoachiever/Agent.java create mode 100644 worker/src/main/java/com/repoachiever/App.java create mode 100644 worker/src/main/java/com/repoachiever/converter/CronExpressionConverter.java create mode 100644 worker/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java create mode 100644 worker/src/main/java/com/repoachiever/entity/ConfigEntity.java create mode 100644 worker/src/main/java/com/repoachiever/entity/PropertiesEntity.java create mode 100644 worker/src/main/java/com/repoachiever/exception/CommandExecutorException.java create mode 100644 worker/src/main/java/com/repoachiever/exception/ConfigValidationException.java create mode 100644 worker/src/main/java/com/repoachiever/exception/CronExpressionException.java create mode 100644 worker/src/main/java/com/repoachiever/exception/KafkaProducerSendException.java create mode 100644 worker/src/main/java/com/repoachiever/logging/FatalAppender.java create mode 100644 worker/src/main/java/com/repoachiever/service/config/ConfigService.java create mode 100644 worker/src/main/java/com/repoachiever/service/config/common/ValidConfigService.java create mode 100644 worker/src/main/java/com/repoachiever/service/listener/ClusterListenerService.java create mode 100644 worker/src/main/java/com/repoachiever/service/scheduler/SchedulerService.java create mode 100644 worker/src/main/java/com/repoachiever/service/scheduler/command/ExecCommandService.java create mode 100644 worker/src/main/java/com/repoachiever/service/scheduler/executor/CommandExecutorService.java create mode 100644 worker/src/main/java/com/repoachiever/service/vendor/VendorFacade.java create mode 100644 worker/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java create mode 100644 worker/src/main/java/com/repoachiever/service/vendor/git/local/GitLocalVendorService.java create mode 100644 worker/src/main/java/com/repoachiever/service/waiter/WaiterHelper.java create mode 100644 worker/src/main/resources/application.properties create mode 100644 worker/src/main/resources/log4j2.xml diff --git a/.gitignore b/.gitignore index 524f096..69a95bd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Idea + +.idea/ diff --git a/LICENSE b/LICENSE index b737d16..6689009 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Yaroslav Svitlytskyi +Copyright (c) 2022 YarikRevich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e699bd --- /dev/null +++ b/Makefile @@ -0,0 +1,135 @@ +dev := $(or $(dev), 'false') + +ifneq (,$(wildcard .env)) +include .env +export +endif + +.PHONY: help +.DEFAULT_GOAL := help +help: + @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean project area + @mvn clean + +.PHONY: prepare +prepare: ## Install prerequisites + @mvn org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree -Dverbose=true + +.PHONY: test +test: clean ## Run both unit and integration tests + @mvn test + @mvn verify + +.PHONY: lint +lint: ## Run Apache Spotless linter + @mvn spotless:apply + +.PHONY: create-local-client +create-local-client: ## Create ResourceTracker local directory for client + @mkdir -p $(HOME)/.repoachiever/config + +.PHONY: create-local-api-server +create-local-api-server: ## Create ResourceTracker local directory for API Server + @mkdir -p $(HOME)/.repoachiever/config + @mkdir -p $(HOME)/.repoachiever/diagnostics/prometheus/internal + @mkdir -p $(HOME)/.repoachiever/diagnostics/prometheus/config + @mkdir -p $(HOME)/.repoachiever/diagnostics/grafana/internal + @mkdir -p $(HOME)/.repoachiever/diagnostics/grafana/config/dashboards + @mkdir -p $(HOME)/.repoachiever/diagnostics/grafana/config/datasources + @mkdir -p $(HOME)/.repoachiever/workspace + @mkdir -p $(HOME)/.repoachiever/internal/database + @mkdir -p $(HOME)/.repoachiever/internal/state + +.PHONY: clone-client-config +clone-client-config: ## Clone configuration files to local directory + @cp -r ./samples/config/client/user.yaml $(HOME)/.repoachiever/config + +.PHONY: clone-api-server-config +clone-api-server-config: ## Clone RepoAchiever API Server configuration files to local directory + @cp -r ./config/grafana/dashboards/dashboard.yml $(HOME)/.repoachiever/diagnostics/grafana/config/dashboards + @cp -r ./config/grafana/dashboards/diagnostics.tmpl $(HOME)/.repoachiever/diagnostics/grafana/config/dashboards + @cp -r ./config/grafana/datasources/datasource.tmpl $(HOME)/.repoachiever/diagnostics/grafana/config/datasources + @cp -r ./config/prometheus/prometheus.tmpl $(HOME)/.repoachiever/diagnostics/prometheus/config + @cp -r ./samples/config/api-server/api-server.yaml $(HOME)/.repoachiever/config + +.PHONY: clone-worker +clone-worker: ## Clone Worker JAR into a RepoAchiever local directory +ifeq (,$(wildcard $(HOME)/.repoachiever/bin/worker)) + @mkdir -p $(HOME)/.repoachiever/bin +endif + @cp -r ./bin/worker $(HOME)/.repoachiever/bin/ + +.PHONY: clone-cluster +clone-cluster: ## Clone Cluster JAR into a RepoAchiever local directory +ifeq (,$(wildcard $(HOME)/.repoachiever/bin/cluster)) + @mkdir -p $(HOME)/.repoachiever/bin +endif + @cp -r ./bin/cluster $(HOME)/.repoachiever/bin/ + +.PHONY: clone-api-server +clone-api-server: ## Clone API Server JAR into a RepoAchiever local directory +ifeq (,$(wildcard $(HOME)/.repoachiever/bin/api-server)) + @mkdir -p $(HOME)/.repoachiever/bin +endif + @cp -r ./bin/api-server $(HOME)/.repoachiever/bin/ + +.PHONY: build-worker +build-worker: clean ## Build Worker application +ifneq (,$(wildcard ./bin/worker)) + @rm -r ./bin/worker +endif +ifeq ($(dev), 'false') + @mvn -pl worker -T10 install +else + @mvn -P dev -pl worker -T10 install +endif + $(MAKE) clone-worker + +.PHONY: build-cluster +build-cluster: clean ## Build Cluster application +ifneq (,$(wildcard ./bin/cluster)) + @rm -r ./bin/cluster +endif +ifeq ($(dev), 'false') + @mvn -pl cluster -T10 install +else + @mvn -P dev -pl cluster -T10 install +endif + $(MAKE) clone-cluster + +.PHONY: build-api-server +build-api-server: clean create-local-api-server clone-api-server-config ## Build API Server application +ifneq (,$(wildcard ./bin/api-server)) + @rm -r ./bin/api-server +endif +ifeq ($(dev), 'false') + @mvn -pl api-server -T10 install +else + @mvn -P dev -pl api-server -T10 install +endif + $(MAKE) clone-api-server + +.PHONY: build-cli +build-cli: clean create-local-client clone-client-config ## Build CLI application +ifneq (,$(wildcard ./bin/cli)) + @rm -r ./bin/cli +endif +ifeq ($(dev), 'false') + @mvn -pl cli -T10 install +else + @mvn -P dev -pl cli -T10 install +endif + +.PHONY: build-gui +build-gui: clean create-local-client clone-client-config create-local-api-server build-api-server ## Build GUI application +ifneq (,$(wildcard ./bin/gui)) + @rm -r ./bin/gui +endif +ifeq ($(dev), 'false') + @mvn -pl gui -T10 install +else + @mvn -P dev -pl gui -T10 install +endif \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d3476b --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# RepoAchiever + +[![Build](https://github.com/YarikRevich/ResourceTracker/actions/workflows/build.yml/badge.svg)](https://github.com/YarikRevich/ResourceTracker/actions/workflows/build.yml) +![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) +![MacOS](https://img.shields.io/badge/MacOS-8773f5?style=for-the-badge&logo=macos&logoColor=black) +[![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) + +## General Information + +A tool used for remote Git repositories achieving operations. + +![](./docs/high-level-design.png) + +![](./docs/detailed-design.png) + +![](./docs/workspace-design.png) + +RepoAchiever uses server-side data validation. It means, that on client side no data transformation or further analysis are made. +Only API Server calls are on client side responsibility. + +## Setup + +All setup related operations are processed via **Makefile** placed in the root directory. + +### CLI + +In order to build CLI it's required to execute the following command. Initially it cleans the environment and builds Java project using **Maven** +```shell +make build-cli +``` + +After the execution of command given above the executable will be generated and placed into **bin** folder in the root directory of the project + +**CLI** build automatically places default **user.yaml** configuration file into ~/.resourcetracker/config directory. + +### GUI + +In order to build GUI it's required to execute the following command. Initially it cleans the environment and build Java project using **Maven** +```shell +make build-gui +``` + +After the execution of command given above the executable will be generated and placed into **bin** folder in the root directory of the project + +**GUI** build automatically compiles **API Server** and places both executable JAR and other dependencies into **~/.resourcetracker/bin/api-server** directory + +It's highly recommended not to move **API Server** files from the default local directory + +### API Server + +In order to build **API Server** it's required to execute the following command. Initially it cleans the environment and build Java project using **Maven** +```shell +make build-api-server +``` + +After the execution of command given above the executable will be generated and placed into **bin** folder in the root directory of the project + +## Use cases + +For both **CLI** and **GUI** examples, there was used the following user configuration file: +```yaml +requests: + - name: "first" + frequency: "10 * * * * *" + file: "/Volumes/Files/first.sh" +cloud: + provider: "aws" + credentials: + file: "/Volumes/Files/aws.csv" + region: "us-west-2" +api-server: + host: "http://localhost:8080" +``` + +And the following request script file: +```shell +#!/bin/bash + +echo "Hello world!" +``` + +### CLI + +![cli](./docs/example/cli.gif) + +### GUI + +![gui](./docs/example/gui.gif) \ No newline at end of file diff --git a/api-server/api-server.iml b/api-server/api-server.iml new file mode 100644 index 0000000..258b38a --- /dev/null +++ b/api-server/api-server.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/api-server/pom.xml b/api-server/pom.xml new file mode 100644 index 0000000..ee77cba --- /dev/null +++ b/api-server/pom.xml @@ -0,0 +1,317 @@ + + + 4.0.0 + api-server + 1.0-SNAPSHOT + api-server + API Server for RepoAchiever + + + com.repoachiever + base + 1.0-SNAPSHOT + + + + + + Shell-Command-Executor-Lib + Shell-Command-Executor-Lib + system + ${basedir}/../lib/Shell-Command-Executor-Lib-0.5.0-SNAPSHOT.jar + + + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + io.quarkus + quarkus-vertx-http + + + io.quarkus + quarkus-vertx-http-deployment + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-liquibase + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-agroal + + + io.quarkiverse.jdbc + quarkus-jdbc-sqlite + + + + + org.springframework + spring-core + + + + + io.pebbletemplates + pebble + + + org.jboss + jandex + + + org.jboss.logging + jboss-logging + + + jakarta.json + jakarta.json-api + + + jakarta.activation + jakarta.activation-api + + + jakarta.transaction + jakarta.transaction-api + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.projectlombok + lombok + + + org.osgi + org.osgi.annotation + + + org.yaml + snakeyaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + commons-io + commons-io + + + com.opencsv + opencsv + + + org.jetbrains + annotations + + + + + org.freemarker + freemarker + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + org.hibernate.validator + hibernate-validator + + + + + api-server + + + io.quarkus.platform + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + org.openapitools + openapi-generator-maven-plugin + + + + generate + + + ${project.basedir}/../api-server/src/main/openapi/openapi.yml + jaxrs-spec + quarkus + ${default.package}.api + ${default.package}.model + true + false + false + false + false + false + true + false + + @lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + src/main/java + true + true + true + true + true + false + true + java8 + true + true + + + + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + + + + ${basedir}/target/quarkus-app/quarkus-run.jar + ${main.basedir}/../bin/api-server/api-server.jar + + + + + + pl.project13.maven + git-commit-id-plugin + + + org.codehaus.mojo + exec-maven-plugin + + + copy-quarkus-lib + package + + exec + + + cp + + -r + ${basedir}/target/quarkus-app/lib + ${main.basedir}/../bin/api-server/lib + + + + + copy-quarkus-quarkus + package + + exec + + + cp + + -r + ${basedir}/target/quarkus-app/quarkus + ${main.basedir}/../bin/api-server/quarkus + + + + + copy-quarkus-app + package + + exec + + + cp + + -r + ${basedir}/target/quarkus-app/app + ${main.basedir}/../bin/api-server/app + + + + + + + + diff --git a/api-server/src/main/java/com/repoachiever/converter/ClusterContextToJsonConverter.java b/api-server/src/main/java/com/repoachiever/converter/ClusterContextToJsonConverter.java new file mode 100644 index 0000000..dee85cc --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/converter/ClusterContextToJsonConverter.java @@ -0,0 +1,31 @@ +package com.repoachiever.converter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.repoachiever.entity.common.ClusterContextEntity; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Represents RepoAchiever Cluster context entity to JSON converter. + */ +public class ClusterContextToJsonConverter { + private static final Logger logger = LogManager.getLogger(ClusterContextToJsonConverter.class); + + public static String convert(ClusterContextEntity content) { + ObjectMapper mapper = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + + try { + return mapper.writeValueAsString(content); + } catch (JsonProcessingException e) { + logger.fatal(e.getMessage()); + } + + return null; + } +} diff --git a/api-server/src/main/java/com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.java b/api-server/src/main/java/com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.java new file mode 100644 index 0000000..ced666d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.java @@ -0,0 +1,18 @@ +package com.repoachiever.converter; + +import com.repoachiever.model.Provider; +import com.repoachiever.model.CredentialsFieldsExternal; +import com.repoachiever.entity.common.ClusterContextEntity; + +/** + * Represents RepoAchiever Cluster content credentials to context credentials converter. + */ +public class ContentCredentialsToClusterContextCredentialsConverter { + public static ClusterContextEntity.Service.Credentials convert( + Provider provider, CredentialsFieldsExternal credentialsFieldsExternal) { + return switch (provider) { + case LOCAL -> null; + case GITHUB -> ClusterContextEntity.Service.Credentials.of(credentialsFieldsExternal.getToken()); + }; + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.java b/api-server/src/main/java/com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.java new file mode 100644 index 0000000..cf7c977 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.java @@ -0,0 +1,24 @@ +package com.repoachiever.converter; + +import com.repoachiever.entity.common.ClusterContextEntity; +import com.repoachiever.model.Provider; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Represents RepoAchiever Cluster content provider to context provider converter. + */ +public class ContentProviderToClusterContextProviderConverter { + public static ClusterContextEntity.Service.Provider convert(Provider contentProvider) { + return ClusterContextEntity.Service.Provider.valueOf( + Arrays.stream( + ClusterContextEntity.Service.Provider.values()) + .toList() + .stream() + .filter(element -> Objects.equals(element.toString(), contentProvider.toString())) + .map(Enum::name) + .toList() + .get(0)); + } +} diff --git a/api-server/src/main/java/com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.java b/api-server/src/main/java/com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.java new file mode 100644 index 0000000..5619e82 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.java @@ -0,0 +1,44 @@ +package com.repoachiever.converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.repoachiever.model.ReadinessCheckResult; +import java.io.IOException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.health.HealthCheckResponse; + +/** + * Represents health check response to readiness result converter. + */ +public class HealthCheckResponseToReadinessCheckResult { + private static final Logger logger = + LogManager.getLogger(HealthCheckResponseToReadinessCheckResult.class); + + /** + * Converts given health check response to readiness check result entity. + * + * @param content given content to be converted. + * @return converted readiness check result entity. + */ + public static ReadinessCheckResult convert(HealthCheckResponse content) { + ObjectMapper mapper = new ObjectMapper(); + + String contentRaw = null; + try { + contentRaw = mapper.writeValueAsString(content); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + + ObjectReader reader = mapper.reader().forType(new TypeReference() {}); + try { + return reader.readValues(contentRaw).readAll().getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + + return null; + } +} diff --git a/api-server/src/main/java/com/repoachiever/dto/ClusterAllocationDto.java b/api-server/src/main/java/com/repoachiever/dto/ClusterAllocationDto.java new file mode 100644 index 0000000..9fb898d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/dto/ClusterAllocationDto.java @@ -0,0 +1,31 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents RepoAchiever Cluster allocation. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ClusterAllocationDto { + /** + * Represents name of RepoAchiever Cluster allocation. + */ + private String name; + + /** + * Represents process identificator of RepoAchiever Cluster allocation. + */ + private Integer pid; + + /** + * Represents context used for RepoAchiever Cluster configuration. + */ + private String context; + + /** + * Represents workspace unit key used to target RepoAchiever Cluster results. + */ + private String workspaceUnitKey; +} diff --git a/api-server/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java b/api-server/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java new file mode 100644 index 0000000..870ca95 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java @@ -0,0 +1,15 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents command executor output. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class CommandExecutorOutputDto { + private String normalOutput; + + private String errorOutput; +} diff --git a/api-server/src/main/java/com/repoachiever/dto/RepositoryContentUnitDto.java b/api-server/src/main/java/com/repoachiever/dto/RepositoryContentUnitDto.java new file mode 100644 index 0000000..dd9633f --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/dto/RepositoryContentUnitDto.java @@ -0,0 +1,20 @@ +package com.repoachiever.dto; + +import com.repoachiever.entity.common.ClusterContextEntity; +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents repository content unit. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class RepositoryContentUnitDto { + private String location; + + private Provider provider; + + private CredentialsFieldsFull credentials; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/entity/common/ClusterContextEntity.java b/api-server/src/main/java/com/repoachiever/entity/common/ClusterContextEntity.java new file mode 100644 index 0000000..7a8d1aa --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/common/ClusterContextEntity.java @@ -0,0 +1,145 @@ +package com.repoachiever.entity.common; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents context sent to RepoAchiever Cluster as a variable during startup operation. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ClusterContextEntity { + /** + * Contains metadata for a specific RepoAchiever Cluster allocation. + */ + @AllArgsConstructor(staticName = "of") + public static class Metadata { + @JsonProperty("name") + public String name; + + @JsonProperty("workspace_unit_key") + public String workspaceUnitKey; + } + + @JsonProperty("metadata") + public Metadata metadata; + + /** + * Represents filter section elected for a specific RepoAchiever Cluster allocation. + */ + @AllArgsConstructor(staticName = "of") + public static class Filter { + @JsonProperty("locations") + public List locations; + } + + @JsonProperty("filter") + public Filter filter; + + /** + * Represents external service configurations for RepoAchiever Cluster allocation used to retrieve content. + */ + @AllArgsConstructor(staticName = "of") + public static class Service { + /** + * Represents all supported service providers, which can be used by RepoAchiever Cluster allocation. + */ + public enum Provider { + LOCAL("git-local"), + GITHUB("git-github"); + + private final String value; + + Provider(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + @JsonProperty("provider") + public Provider provider; + + /** + * Represents credentials used for external service communication by RepoAchiever Cluster allocation. + */ + @AllArgsConstructor(staticName = "of") + public static class Credentials { + @JsonProperty("token") + public String token; + } + + @JsonProperty("credentials") + public Credentials credentials; + } + + @JsonProperty("service") + public Service service; + + /** + * Represents RepoAchiever Cluster configuration used for internal communication infrastructure setup. + */ + @AllArgsConstructor(staticName = "of") + public static class Communication { + @NotNull + @JsonProperty("api_server_name") + public String apiServerName; + + @JsonProperty("port") + public Integer port; + } + + @JsonProperty("communication") + public Communication communication; + + /** + * Represents RepoAchiever Cluster configuration used for content management. + */ + @AllArgsConstructor(staticName = "of") + public static class Content { + @JsonProperty("format") + public String format; + } + + @JsonProperty("content") + public Content content; + + /** + * Represents RepoAchiever API Server resources configuration section. + */ + @AllArgsConstructor(staticName = "of") + public static class Resource { + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Cluster. + */ + @AllArgsConstructor(staticName = "of") + public static class Cluster { + @JsonProperty("max-workers") + public Integer maxWorkers; + } + + @JsonProperty("cluster") + public Cluster cluster; + + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Worker. + */ + @AllArgsConstructor(staticName = "of") + public static class Worker { + @JsonProperty("frequency") + public String frequency; + } + + @JsonProperty("worker") + public Worker worker; + } + + @JsonProperty("resource") + public Resource resource; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/entity/common/ConfigEntity.java b/api-server/src/main/java/com/repoachiever/entity/common/ConfigEntity.java new file mode 100644 index 0000000..2410b66 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/common/ConfigEntity.java @@ -0,0 +1,197 @@ +package com.repoachiever.entity.common; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; + +/** + * Represents configuration model used for RepoAchiever API Server operations. + */ +@Getter +@ApplicationScoped +public class ConfigEntity { + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever API Server instance setup. + */ + @Getter + public static class Connection { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("connection") + public Connection connection; + + /** + * Represents RepoAchiever API Server configuration used for internal communication infrastructure setup. + */ + @Getter + public static class Communication { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("communication") + public Communication communication; + + /** + * Represents RepoAchiever API Server configuration used for content management. + */ + @Getter + public static class Content { + @NotNull + @Pattern(regexp = "(^zip$)|(^tar$)") + @JsonProperty("format") + public String format; + } + + @Valid + @NotNull + @JsonProperty("content") + public Content content; + + /** + * Represents RepoAchiever API Server configuration used for database setup. + */ + @Getter + public static class Database { + @NotNull + @JsonProperty("re-init") + public Boolean reInit; + } + + @Valid + @NotNull + @JsonProperty("database") + public Database database; + + /** + * Represents RepoAchiever API Server configuration used for diagnostics. + */ + @Getter + public static class Diagnostics { + @NotNull + @JsonProperty("enabled") + public Boolean enabled; + + /** + * Represents RepoAchiever API Server metrics configuration setup. + */ + @Getter + public static class Metrics { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("metrics") + public Metrics metrics; + + /** + * Represents RepoAchiever API Server configuration used for Grafana instance setup. + */ + @Getter + public static class Grafana { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("grafana") + public Grafana grafana; + + /** + * Represents RepoAchiever API Server configuration used for Prometheus instance setup. + */ + @Getter + public static class Prometheus { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("prometheus") + public Prometheus prometheus; + + /** + * Represents RepoAchiever API Server configuration used for Prometheus Node Exporter instance setup. + */ + @Getter + public static class NodeExporter { + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("node-exporter") + public NodeExporter nodeExporter; + } + + @Valid + @NotNull + @JsonProperty("diagnostics") + public Diagnostics diagnostics; + + /** + * Represents RepoAchiever API Server resources configuration section. + */ + @Getter + public static class Resource { + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Cluster. + */ + @Getter + public static class Cluster { + @NotNull + @JsonProperty("max-workers") + public Integer maxWorkers; + } + + @Valid + @NotNull + @JsonProperty("cluster") + public Cluster cluster; + + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Worker. + */ + @Getter + public static class Worker { + @NotNull + @Pattern( + regexp = + "(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\\?])|([\\*]))[\\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\\?])|([\\*]))[\\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\\?])|([\\*]))[\\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\\?])|([\\*]))([\\s]?(([\\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?|" + + " (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?|" + + " ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)") + @JsonProperty("frequency") + public String frequency; + } + + @Valid + @NotNull + @JsonProperty("worker") + public Worker worker; + } + + @Valid + @NotNull + @JsonProperty("resource") + public Resource resource; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/entity/common/MetadataFileEntity.java b/api-server/src/main/java/com/repoachiever/entity/common/MetadataFileEntity.java new file mode 100644 index 0000000..330715c --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/common/MetadataFileEntity.java @@ -0,0 +1,20 @@ +package com.repoachiever.entity.common; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * Represents metadata file, including pull requests, issues and releases, which are pulled from selected resource provider. + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor(staticName = "of") +public class MetadataFileEntity { + @JsonProperty("data") + private String data; + + @JsonProperty("hash") + private String hash; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/entity/common/PropertiesEntity.java b/api-server/src/main/java/com/repoachiever/entity/common/PropertiesEntity.java new file mode 100644 index 0000000..24bbb98 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/common/PropertiesEntity.java @@ -0,0 +1,166 @@ +package com.repoachiever.entity.common; + +import jakarta.enterprise.context.ApplicationScoped; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Exposes access to properties setup to be used for further configuration. + */ +@Getter +@ApplicationScoped +public class PropertiesEntity { + @ConfigProperty(name = "quarkus.application.version") + String applicationVersion; + + @ConfigProperty(name = "quarkus.http.host") + String applicationHost; + + @ConfigProperty(name = "quarkus.http.port") + Integer applicationPort; + + @ConfigProperty(name = "state.location") + String stateLocation; + + @ConfigProperty(name = "state.running.name") + String stateRunningName; + + @ConfigProperty(name = "database.tables.config.name") + String databaseConfigTableName; + + @ConfigProperty(name = "database.tables.content.name") + String databaseContentTableName; + + @ConfigProperty(name = "database.tables.provider.name") + String databaseProviderTableName; + + @ConfigProperty(name = "database.tables.secret.name") + String databaseSecretTableName; + + @ConfigProperty(name = "database.statement.close-delay") + Integer databaseStatementCloseDelay; + + @ConfigProperty(name = "bin.directory") + String binDirectory; + + @ConfigProperty(name = "bin.cluster.location") + String binClusterLocation; + + @ConfigProperty(name = "config.directory") + String configDirectory; + + @ConfigProperty(name = "config.name") + String configName; + + @ConfigProperty(name = "workspace.directory") + String workspaceDirectory; + + @ConfigProperty(name = "workspace.content.directory") + String workspaceContentDirectory; + + @ConfigProperty(name = "workspace.content.version-amount") + Integer workspaceContentVersionAmount; + + @ConfigProperty(name = "workspace.metadata.directory") + String workspaceMetadataDirectory; + + @ConfigProperty(name = "workspace.prs-metadata-file.name") + String workspacePRsMetadataFileName; + + @ConfigProperty(name = "workspace.issues-metadata-file.name") + String workspaceIssuesMetadataFileName; + + @ConfigProperty(name = "workspace.releases-metadata-file.name") + String workspaceReleasesMetadataFileName; + + @ConfigProperty(name = "repoachiever-cluster.context.alias") + String clusterContextAlias; + + @ConfigProperty(name = "communication.api-server.name") + String communicationApiServerName; + + @ConfigProperty(name = "communication.cluster.base") + String communicationClusterBase; + + @ConfigProperty(name = "communication.cluster.startup-await-frequency") + Integer communicationClusterStartupAwaitFrequency; + + @ConfigProperty(name = "communication.cluster.startup-timeout") + Integer communicationClusterStartupTimeout; + + @ConfigProperty(name = "communication.cluster.health-check-frequency") + Integer communicationClusterHealthCheckFrequency; + + @ConfigProperty(name = "diagnostics.common.docker.network.name") + String diagnosticsCommonDockerNetworkName; + + @ConfigProperty(name = "diagnostics.grafana.config.location") + String diagnosticsGrafanaConfigLocation; + + @ConfigProperty(name = "diagnostics.grafana.datasources.location") + String diagnosticsGrafanaDatasourcesLocation; + + @ConfigProperty(name = "diagnostics.grafana.datasources.template") + String diagnosticsGrafanaDatasourcesTemplate; + + @ConfigProperty(name = "diagnostics.grafana.datasources.output") + String diagnosticsGrafanaDatasourcesOutput; + + @ConfigProperty(name = "diagnostics.grafana.dashboards.location") + String diagnosticsGrafanaDashboardsLocation; + + @ConfigProperty(name = "diagnostics.grafana.dashboards.diagnostics.template") + String diagnosticsGrafanaDashboardsDiagnosticsTemplate; + + @ConfigProperty(name = "diagnostics.grafana.dashboards.diagnostics.output") + String diagnosticsGrafanaDashboardsDiagnosticsOutput; + + @ConfigProperty(name = "diagnostics.grafana.internal.location") + String diagnosticsGrafanaInternalLocation; + + @ConfigProperty(name = "diagnostics.grafana.docker.name") + String diagnosticsGrafanaDockerName; + + @ConfigProperty(name = "diagnostics.grafana.docker.image") + String diagnosticsGrafanaDockerImage; + + @ConfigProperty(name = "diagnostics.prometheus.config.location") + String diagnosticsPrometheusConfigLocation; + + @ConfigProperty(name = "diagnostics.prometheus.config.template") + String diagnosticsPrometheusConfigTemplate; + + @ConfigProperty(name = "diagnostics.prometheus.config.output") + String diagnosticsPrometheusConfigOutput; + + @ConfigProperty(name = "diagnostics.prometheus.internal.location") + String diagnosticsPrometheusInternalLocation; + + @ConfigProperty(name = "diagnostics.prometheus.docker.name") + String diagnosticsPrometheusDockerName; + + @ConfigProperty(name = "diagnostics.prometheus.docker.image") + String diagnosticsPrometheusDockerImage; + + @ConfigProperty(name = "diagnostics.prometheus.node-exporter.docker.name") + String diagnosticsPrometheusNodeExporterDockerName; + + @ConfigProperty(name = "diagnostics.prometheus.node-exporter.docker.image") + String diagnosticsPrometheusNodeExporterDockerImage; + + @ConfigProperty(name = "diagnostics.metrics.connection.timeout") + Integer diagnosticsMetricsConnectionTimeout; + + @ConfigProperty(name = "git.commit.id.abbrev") + String gitCommitId; + + /** + * Removes the last symbol in git commit id of the repository. + * + * @return chopped repository git commit id. + */ + public String getGitCommitId() { + return StringUtils.chop(gitCommitId); + } +} diff --git a/api-server/src/main/java/com/repoachiever/entity/repository/ConfigEntity.java b/api-server/src/main/java/com/repoachiever/entity/repository/ConfigEntity.java new file mode 100644 index 0000000..f615fdb --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/repository/ConfigEntity.java @@ -0,0 +1,22 @@ +package com.repoachiever.entity.repository; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents entity used to describe added configuration. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ConfigEntity { + private Integer id; + + private String name; + + private String hash; +} diff --git a/api-server/src/main/java/com/repoachiever/entity/repository/ContentEntity.java b/api-server/src/main/java/com/repoachiever/entity/repository/ContentEntity.java new file mode 100644 index 0000000..df8acf5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/repository/ContentEntity.java @@ -0,0 +1,20 @@ +package com.repoachiever.entity.repository; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents entity used to describe registered locations. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ContentEntity { + private Integer id; + + private String location; + + private Integer provider; + + private Integer secret; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/entity/repository/ProviderEntity.java b/api-server/src/main/java/com/repoachiever/entity/repository/ProviderEntity.java new file mode 100644 index 0000000..45b69e1 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/repository/ProviderEntity.java @@ -0,0 +1,15 @@ +package com.repoachiever.entity.repository; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents entity used to describe available providers. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ProviderEntity { + private Integer id; + + private String name; +} diff --git a/api-server/src/main/java/com/repoachiever/entity/repository/SecretEntity.java b/api-server/src/main/java/com/repoachiever/entity/repository/SecretEntity.java new file mode 100644 index 0000000..1848a3f --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/entity/repository/SecretEntity.java @@ -0,0 +1,23 @@ +package com.repoachiever.entity.repository; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Optional; + +/** + * Represents entity used to describe registered secrets. + */ +@Getter +@AllArgsConstructor(staticName = "of") +public class SecretEntity { + private Integer id; + + private Integer session; + + private Optional credentials; +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.java b/api-server/src/main/java/com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.java new file mode 100644 index 0000000..af511da --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever API Server is already running. + */ +public class ApiServerInstanceIsAlreadyRunningException extends IOException { + public ApiServerInstanceIsAlreadyRunningException() { + this(""); + } + + public ApiServerInstanceIsAlreadyRunningException(Object... message) { + super( + new Formatter() + .format("RepoAchiever API Server instance is already running: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationFailureException.java new file mode 100644 index 0000000..61026f4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster application fails. + */ +public class ClusterApplicationFailureException extends IOException { + public ClusterApplicationFailureException() { + this(""); + } + + public ClusterApplicationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster application failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationTimeoutException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationTimeoutException.java new file mode 100644 index 0000000..c85b75c --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationTimeoutException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster application received timeout. + */ +public class ClusterApplicationTimeoutException extends IOException { + public ClusterApplicationTimeoutException() { + this(""); + } + + public ClusterApplicationTimeoutException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster application received timeout: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterDeploymentFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterDeploymentFailureException.java new file mode 100644 index 0000000..ed00608 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterDeploymentFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster deployment fails. + */ +public class ClusterDeploymentFailureException extends IOException { + public ClusterDeploymentFailureException() { + this(""); + } + + public ClusterDeploymentFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster deployment failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterDestructionFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterDestructionFailureException.java new file mode 100644 index 0000000..bb184e8 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterDestructionFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster destruction fails. + */ +public class ClusterDestructionFailureException extends IOException { + public ClusterDestructionFailureException() { + this(""); + } + + public ClusterDestructionFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster destruction failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterFullDestructionFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterFullDestructionFailureException.java new file mode 100644 index 0000000..6e0e55d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterFullDestructionFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster full destruction fails. + */ +public class ClusterFullDestructionFailureException extends IOException { + public ClusterFullDestructionFailureException() { + this(""); + } + + public ClusterFullDestructionFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster full destruction failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterOperationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterOperationFailureException.java new file mode 100644 index 0000000..773ece1 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterOperationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster operation fails. + */ +public class ClusterOperationFailureException extends IOException { + public ClusterOperationFailureException() { + this(""); + } + + public ClusterOperationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterRecreationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterRecreationFailureException.java new file mode 100644 index 0000000..af0c319 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterRecreationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster recreation operation fails. + */ +public class ClusterRecreationFailureException extends IOException { + public ClusterRecreationFailureException() { + this(""); + } + + public ClusterRecreationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster recreation operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.java new file mode 100644 index 0000000..07556be --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster unhealthy allocation reapplication fails. + */ +public class ClusterUnhealthyReapplicationFailureException extends IOException { + public ClusterUnhealthyReapplicationFailureException() { + this(""); + } + + public ClusterUnhealthyReapplicationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster unhealthy allocation reapplication failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ClusterWithdrawalFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ClusterWithdrawalFailureException.java new file mode 100644 index 0000000..ce699d5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ClusterWithdrawalFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever Cluster withdrawal fails. + */ +public class ClusterWithdrawalFailureException extends IOException { + public ClusterWithdrawalFailureException() { + this(""); + } + + public ClusterWithdrawalFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster withdrawal failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/CommandExecutorException.java b/api-server/src/main/java/com/repoachiever/exception/CommandExecutorException.java new file mode 100644 index 0000000..6cf46ee --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/CommandExecutorException.java @@ -0,0 +1,17 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when command execution process fails. + */ +public class CommandExecutorException extends IOException { + public CommandExecutorException(Object... message) { + super( + new Formatter() + .format("Invalid command executor behaviour: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java new file mode 100644 index 0000000..1f09d8e --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when communication configuration process fails. + */ +public class CommunicationConfigurationFailureException extends IOException { + public CommunicationConfigurationFailureException() { + this(""); + } + + public CommunicationConfigurationFailureException(Object... message) { + super( + new Formatter() + .format("Communication configuration operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/ConfigValidationException.java b/api-server/src/main/java/com/repoachiever/exception/ConfigValidationException.java new file mode 100644 index 0000000..a8ffa9d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ConfigValidationException.java @@ -0,0 +1,17 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when configuration file validation process fails. + */ +public class ConfigValidationException extends IOException { + public ConfigValidationException(Object... message) { + super( + new Formatter() + .format("Config file content is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ContentApplicationRetrievalFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ContentApplicationRetrievalFailureException.java new file mode 100644 index 0000000..79961d4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ContentApplicationRetrievalFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when content application retrieval process fails. + */ +public class ContentApplicationRetrievalFailureException extends IOException { + public ContentApplicationRetrievalFailureException() { + this(""); + } + + public ContentApplicationRetrievalFailureException(Object... message) { + super( + new Formatter() + .format("Content application retrieval failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ContentFileNotFoundException.java b/api-server/src/main/java/com/repoachiever/exception/ContentFileNotFoundException.java new file mode 100644 index 0000000..290cd6d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ContentFileNotFoundException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when configuration file content was not found. + */ +public class ContentFileNotFoundException extends IOException { + public ContentFileNotFoundException() { + this(""); + } + + public ContentFileNotFoundException(Object... message) { + super( + new Formatter() + .format("Content file is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/ContentFileRemovalFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ContentFileRemovalFailureException.java new file mode 100644 index 0000000..576f488 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ContentFileRemovalFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when configuration file content removal operation fails. + */ +public class ContentFileRemovalFailureException extends IOException { + public ContentFileRemovalFailureException() { + this(""); + } + + public ContentFileRemovalFailureException(Object... message) { + super( + new Formatter() + .format("Content file removal failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/ContentFileWriteFailureException.java b/api-server/src/main/java/com/repoachiever/exception/ContentFileWriteFailureException.java new file mode 100644 index 0000000..d447974 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/ContentFileWriteFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when content file write operation fails. + */ +public class ContentFileWriteFailureException extends IOException { + public ContentFileWriteFailureException() { + this(""); + } + + public ContentFileWriteFailureException(Object... message) { + super( + new Formatter() + .format("Content file write failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/CredentialsAreNotValidException.java b/api-server/src/main/java/com/repoachiever/exception/CredentialsAreNotValidException.java new file mode 100644 index 0000000..b414463 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/CredentialsAreNotValidException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when given credentials are not valid. + */ +public class CredentialsAreNotValidException extends IOException { + public CredentialsAreNotValidException() { + this(""); + } + + public CredentialsAreNotValidException(Object... message) { + super( + new Formatter() + .format("Credentials are not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/CredentialsConversionException.java b/api-server/src/main/java/com/repoachiever/exception/CredentialsConversionException.java new file mode 100644 index 0000000..791a3f1 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/CredentialsConversionException.java @@ -0,0 +1,17 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when credentials conversion process fails. + */ +public class CredentialsConversionException extends IOException { + public CredentialsConversionException(Object... message) { + super( + new Formatter() + .format("Given credentials are invalid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/CredentialsFieldIsNotValidException.java b/api-server/src/main/java/com/repoachiever/exception/CredentialsFieldIsNotValidException.java new file mode 100644 index 0000000..84cef1e --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/CredentialsFieldIsNotValidException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when credentials field is not valid. + */ +public class CredentialsFieldIsNotValidException extends IOException { + public CredentialsFieldIsNotValidException() { + this(""); + } + + public CredentialsFieldIsNotValidException(Object... message) { + super( + new Formatter() + .format("Credentials field is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.java b/api-server/src/main/java/com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.java new file mode 100644 index 0000000..1691337 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when diagnostics template processing fails. + */ +public class DiagnosticsTemplateProcessingFailureException extends IOException { + public DiagnosticsTemplateProcessingFailureException() { + this(""); + } + + public DiagnosticsTemplateProcessingFailureException(Object... message) { + super( + new Formatter() + .format("Diagnostics template processing failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/DockerInspectRemovalFailureException.java b/api-server/src/main/java/com/repoachiever/exception/DockerInspectRemovalFailureException.java new file mode 100644 index 0000000..dda5a43 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/DockerInspectRemovalFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Docker inspection removal process fails. + */ +public class DockerInspectRemovalFailureException extends IOException { + public DockerInspectRemovalFailureException() { + this(""); + } + + public DockerInspectRemovalFailureException(Object... message) { + super( + new Formatter() + .format("Docker inspect container removal failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/DockerIsNotAvailableException.java b/api-server/src/main/java/com/repoachiever/exception/DockerIsNotAvailableException.java new file mode 100644 index 0000000..c98a154 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/DockerIsNotAvailableException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Docker availability check fails. + */ +public class DockerIsNotAvailableException extends IOException { + public DockerIsNotAvailableException() { + this(""); + } + + public DockerIsNotAvailableException(Object... message) { + super( + new Formatter() + .format("Docker instance is not available: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/DockerNetworkCreateFailureException.java b/api-server/src/main/java/com/repoachiever/exception/DockerNetworkCreateFailureException.java new file mode 100644 index 0000000..21640c9 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/DockerNetworkCreateFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Docker network creation fails. + */ +public class DockerNetworkCreateFailureException extends IOException { + public DockerNetworkCreateFailureException() { + this(""); + } + + public DockerNetworkCreateFailureException(Object... message) { + super( + new Formatter() + .format("Docker network creation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/DockerNetworkRemoveFailureException.java b/api-server/src/main/java/com/repoachiever/exception/DockerNetworkRemoveFailureException.java new file mode 100644 index 0000000..cf55e3a --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/DockerNetworkRemoveFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Docker network removal fails. + */ +public class DockerNetworkRemoveFailureException extends IOException { + public DockerNetworkRemoveFailureException() { + this(""); + } + + public DockerNetworkRemoveFailureException(Object... message) { + super( + new Formatter() + .format("Docker network removal failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/LocationsFieldIsNotValidException.java b/api-server/src/main/java/com/repoachiever/exception/LocationsFieldIsNotValidException.java new file mode 100644 index 0000000..1807859 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/LocationsFieldIsNotValidException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when locations field is not valid. + */ +public class LocationsFieldIsNotValidException extends IOException { + public LocationsFieldIsNotValidException() { + this(""); + } + + public LocationsFieldIsNotValidException(Object... message) { + super( + new Formatter() + .format("Locations field is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/MetadataFileNotFoundException.java b/api-server/src/main/java/com/repoachiever/exception/MetadataFileNotFoundException.java new file mode 100644 index 0000000..069b3fe --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/MetadataFileNotFoundException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when metadata file was not found. + */ +public class MetadataFileNotFoundException extends IOException { + public MetadataFileNotFoundException() { + this(""); + } + + public MetadataFileNotFoundException(Object... message) { + super( + new Formatter() + .format("Metadata file is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/MetadataFileWriteFailureException.java b/api-server/src/main/java/com/repoachiever/exception/MetadataFileWriteFailureException.java new file mode 100644 index 0000000..4747254 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/MetadataFileWriteFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when metadata file write operation fails. + */ +public class MetadataFileWriteFailureException extends IOException { + public MetadataFileWriteFailureException() { + this(""); + } + + public MetadataFileWriteFailureException(Object... message) { + super( + new Formatter() + .format("Metadata file write failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/NodeExporterDeploymentFailureException.java b/api-server/src/main/java/com/repoachiever/exception/NodeExporterDeploymentFailureException.java new file mode 100644 index 0000000..d1c82cd --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/NodeExporterDeploymentFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Prometheus Node Exporter deployment operation fails. + */ +public class NodeExporterDeploymentFailureException extends IOException { + public NodeExporterDeploymentFailureException() { + this(""); + } + + public NodeExporterDeploymentFailureException(Object... message) { + super( + new Formatter() + .format("Prometheus node exporter deployment failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/PrometheusDeploymentFailureException.java b/api-server/src/main/java/com/repoachiever/exception/PrometheusDeploymentFailureException.java new file mode 100644 index 0000000..2e34f8c --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/PrometheusDeploymentFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when Prometheus deployment operation fails. + */ +public class PrometheusDeploymentFailureException extends IOException { + public PrometheusDeploymentFailureException() { + this(""); + } + + public PrometheusDeploymentFailureException(Object... message) { + super( + new Formatter() + .format("Prometheus deployment failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/QueryEmptyResultException.java b/api-server/src/main/java/com/repoachiever/exception/QueryEmptyResultException.java new file mode 100644 index 0000000..4f0bb31 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/QueryEmptyResultException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when database query returns empty result. + */ +public class QueryEmptyResultException extends IOException { + public QueryEmptyResultException() { + this(""); + } + + public QueryEmptyResultException(Object... message) { + super( + new Formatter() + .format("Query result is empty: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/QueryExecutionFailureException.java b/api-server/src/main/java/com/repoachiever/exception/QueryExecutionFailureException.java new file mode 100644 index 0000000..7d90486 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/QueryExecutionFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when database query execution operation fails. + */ +public class QueryExecutionFailureException extends IOException { + public QueryExecutionFailureException() { + this(""); + } + + public QueryExecutionFailureException(Object... message) { + super( + new Formatter() + .format("Query execution failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/RepositoryContentApplicationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/RepositoryContentApplicationFailureException.java new file mode 100644 index 0000000..fc73002 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/RepositoryContentApplicationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when repository content application operation fails. + */ +public class RepositoryContentApplicationFailureException extends IOException { + public RepositoryContentApplicationFailureException() { + this(""); + } + + public RepositoryContentApplicationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster repository content application failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/RepositoryContentDestructionFailureException.java b/api-server/src/main/java/com/repoachiever/exception/RepositoryContentDestructionFailureException.java new file mode 100644 index 0000000..4987054 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/RepositoryContentDestructionFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when repository content destruction operation fails. + */ +public class RepositoryContentDestructionFailureException extends IOException { + public RepositoryContentDestructionFailureException() { + this(""); + } + + public RepositoryContentDestructionFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever Cluster repository content destruction failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/RepositoryOperationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/RepositoryOperationFailureException.java new file mode 100644 index 0000000..e43e3cf --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/RepositoryOperationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when repository operation fails. + */ +public class RepositoryOperationFailureException extends IOException { + public RepositoryOperationFailureException() { + this(""); + } + + public RepositoryOperationFailureException(Object... message) { + super( + new Formatter() + .format("Repository operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/TelemetryOperationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/TelemetryOperationFailureException.java new file mode 100644 index 0000000..3345723 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/TelemetryOperationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when telemetry operation fails. + */ +public class TelemetryOperationFailureException extends IOException { + public TelemetryOperationFailureException() { + this(""); + } + + public TelemetryOperationFailureException(Object... message) { + super( + new Formatter() + .format("Telemetry operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.java new file mode 100644 index 0000000..5cca97b --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when workspace content directory creation operation fails. + */ +public class WorkspaceContentDirectoryCreationFailureException extends IOException { + public WorkspaceContentDirectoryCreationFailureException() { + this(""); + } + + public WorkspaceContentDirectoryCreationFailureException(Object... message) { + super( + new Formatter() + .format("Workspace content directory creation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.java b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.java new file mode 100644 index 0000000..18f1c49 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when workspace unit directory creation operation fails. + */ +public class WorkspaceUnitDirectoryCreationFailureException extends IOException { + public WorkspaceUnitDirectoryCreationFailureException() { + this(""); + } + + public WorkspaceUnitDirectoryCreationFailureException(Object... message) { + super( + new Formatter() + .format("Workspace unit directory creation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.java b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.java new file mode 100644 index 0000000..e9502b5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.nio.file.NoSuchFileException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when workspace unit directory was not found. + */ +public class WorkspaceUnitDirectoryNotFoundException extends NoSuchFileException { + public WorkspaceUnitDirectoryNotFoundException() { + this(""); + } + + public WorkspaceUnitDirectoryNotFoundException(Object... message) { + super( + new Formatter() + .format("Workspace unit is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.java b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.java new file mode 100644 index 0000000..0a07207 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.java @@ -0,0 +1,23 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when workspace unit directory is already present. + */ +public class WorkspaceUnitDirectoryPresentException extends IOException { + public WorkspaceUnitDirectoryPresentException() { + this(""); + } + + public WorkspaceUnitDirectoryPresentException(Object... message) { + super( + new Formatter() + .format( + "Workspace unit is already present, please stop current deployment first: %s", + Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.java b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.java new file mode 100644 index 0000000..6ee978a --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when workspace unit directory removal operation fails. + */ +public class WorkspaceUnitDirectoryRemovalFailureException extends IOException { + public WorkspaceUnitDirectoryRemovalFailureException() { + this(""); + } + + public WorkspaceUnitDirectoryRemovalFailureException(Object... message) { + super( + new Formatter() + .format("Workspace unit directory removal failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/logging/FatalAppender.java b/api-server/src/main/java/com/repoachiever/logging/FatalAppender.java new file mode 100644 index 0000000..a88883f --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/logging/FatalAppender.java @@ -0,0 +1,36 @@ +package com.repoachiever.logging; + +import io.quarkus.runtime.Quarkus; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +/** + * Service used for logging fatal level application state changes. + */ +@Plugin(name = "FatalAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class FatalAppender extends AbstractAppender { + protected FatalAppender(String name, Filter filter) { + super(name, filter, null, false, null); + } + + @PluginFactory + public static FatalAppender createAppender( + @PluginAttribute("name") String name, @PluginElement("Filter") Filter filter) { + return new FatalAppender(name, filter); + } + + @Override + public void append(LogEvent event) { + if (event.getLevel().equals(Level.FATAL)) { + Quarkus.asyncExit(1); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.java new file mode 100644 index 0000000..bbebf78 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.java @@ -0,0 +1,20 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.ClusterApplicationFailureException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Represents mapper for ClusterApplicationFailureException exception. + */ +@Provider +public class ClusterApplicationFailureExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(ClusterApplicationFailureException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.java new file mode 100644 index 0000000..616f84a --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.java @@ -0,0 +1,20 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.ClusterWithdrawalFailureException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Represents mapper for ClusterWithdrawalFailureExceptionMapper exception. + */ +@Provider +public class ClusterWithdrawalFailureExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(ClusterWithdrawalFailureException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.java new file mode 100644 index 0000000..20e15ae --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.java @@ -0,0 +1,18 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.CredentialsAreNotValidException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** Represents mapper for CredentialsAreNotValidException exception. */ +@Provider +public class CredentialsAreNotValidExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(CredentialsAreNotValidException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.java new file mode 100644 index 0000000..ae816bd --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.java @@ -0,0 +1,19 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.CredentialsFieldIsNotValidException; +import com.repoachiever.exception.WorkspaceUnitDirectoryNotFoundException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** Represents mapper for CredentialsFieldIsNotValidException exception. */ +@Provider +public class CredentialsFieldIsNotValidExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(CredentialsFieldIsNotValidException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.java new file mode 100644 index 0000000..1e55c5f --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.java @@ -0,0 +1,18 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.LocationsFieldIsNotValidException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** Represents mapper for LocationsFieldIsNotValidException exception. */ +@Provider +public class LocationsFieldIsNotValidExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(LocationsFieldIsNotValidException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.java new file mode 100644 index 0000000..abea333 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.java @@ -0,0 +1,20 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.RepositoryContentApplicationFailureException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Represents mapper for RepositoryContentApplicationFailureExceptionMapper exception. + */ +@Provider +public class RepositoryContentApplicationFailureExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(RepositoryContentApplicationFailureException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.java new file mode 100644 index 0000000..18911cd --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.java @@ -0,0 +1,20 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.RepositoryContentDestructionFailureException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** + * Represents mapper for RepositoryContentDestructionFailureExceptionMapper exception. + */ +@Provider +public class RepositoryContentDestructionFailureExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(RepositoryContentDestructionFailureException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.java b/api-server/src/main/java/com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.java new file mode 100644 index 0000000..36617a4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.java @@ -0,0 +1,18 @@ +package com.repoachiever.mapping; + +import com.repoachiever.exception.WorkspaceUnitDirectoryNotFoundException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +/** Represents mapper for WorkspaceUnitDirectoryNotFoundException exception. */ +@Provider +public class WorkspaceUnitDirectoryNotFoundExceptionMapper + implements ExceptionMapper { + @Override + public Response toResponse(WorkspaceUnitDirectoryNotFoundException e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()) + .entity(e.getMessage()) + .build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/ConfigRepository.java b/api-server/src/main/java/com/repoachiever/repository/ConfigRepository.java new file mode 100644 index 0000000..cca8115 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/ConfigRepository.java @@ -0,0 +1,123 @@ +package com.repoachiever.repository; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.entity.repository.ConfigEntity; +import com.repoachiever.exception.QueryEmptyResultException; +import com.repoachiever.exception.QueryExecutionFailureException; +import com.repoachiever.exception.RepositoryOperationFailureException; +import com.repoachiever.repository.executor.RepositoryExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Represents repository implementation to handle config table. + */ +@ApplicationScoped +public class ConfigRepository { + @Inject + PropertiesEntity properties; + + @Inject + RepositoryExecutor repositoryExecutor; + + /** + * Inserts given values into the config table. + * + * @param name given name of the configuration. + * @param hash given hash of the configuration. + * @throws RepositoryOperationFailureException if operation execution fails. + */ + public void insert(String name, String hash) throws RepositoryOperationFailureException { + try { + repositoryExecutor.performQuery( + String.format( + "INSERT INTO %s (name, hash) VALUES ('%s', '%s')", + properties.getDatabaseConfigTableName(), + name, + hash)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } + + /** + * Checks if config entity with the given name is present. + * + * @param name given name of the configuration. + * @return result of the check. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public Boolean isPresentByName(String name) throws RepositoryOperationFailureException { + try { + ResultSet resultSet = repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id, t.hash FROM %s as t WHERE t.name = '%s'", + properties.getDatabaseConfigTableName(), + name)); + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } catch (QueryEmptyResultException e) { + return false; + } catch (QueryExecutionFailureException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return true; + } + + /** + * Attempts to retrieve config entity by the given name. + * + * @param name given name of the configuration. + * @return retrieved config entity. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public ConfigEntity findByName(String name) throws RepositoryOperationFailureException { + ResultSet resultSet; + + try { + resultSet = + repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id, t.hash FROM %s as t WHERE t.name = '%s'", + properties.getDatabaseConfigTableName(), + name)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + Integer id; + + try { + id = resultSet.getInt("id"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + String hash; + + try { + hash = resultSet.getString("hash"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return ConfigEntity.of(id, name, hash); + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/ContentRepository.java b/api-server/src/main/java/com/repoachiever/repository/ContentRepository.java new file mode 100644 index 0000000..ccd04d4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/ContentRepository.java @@ -0,0 +1,149 @@ +package com.repoachiever.repository; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.entity.repository.ContentEntity; +import com.repoachiever.entity.repository.ProviderEntity; +import com.repoachiever.exception.QueryEmptyResultException; +import com.repoachiever.exception.QueryExecutionFailureException; +import com.repoachiever.exception.RepositoryOperationFailureException; +import com.repoachiever.repository.executor.RepositoryExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents repository implementation to handle content table. + */ +@ApplicationScoped +public class ContentRepository { + @Inject + PropertiesEntity properties; + + @Inject + RepositoryExecutor repositoryExecutor; + + /** + * Inserts given values into the content table. + * + * @param location given content location. + * @param provider given provider used for content retrieval. + * @param secret given secret, which allows content retrieval. + * @throws RepositoryOperationFailureException if operation execution fails. + */ + public void insert(String location, Integer provider, Integer secret) throws RepositoryOperationFailureException { + try { + repositoryExecutor.performQuery( + String.format( + "INSERT INTO %s (location, provider, secret) VALUES ('%s', %d, %d)", + properties.getDatabaseContentTableName(), + location, + provider, + secret)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } + + /** + * Checks if content entity with the given location is present. + * + * @param location given location of the content. + * @return result of the check. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public Boolean isPresentByLocation(String location) throws RepositoryOperationFailureException { + try { + ResultSet resultSet = repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id FROM %s as t WHERE t.location = '%s'", + properties.getDatabaseContentTableName(), + location)); + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } catch (QueryEmptyResultException e) { + return false; + } catch (QueryExecutionFailureException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return true; + } + + /** + * Retrieves all the persisted content entities. + * + * @return retrieved content entities. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public List findAll() throws RepositoryOperationFailureException { + ResultSet resultSet; + + try { + resultSet = + repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id, t.location, t.provider, t.secret FROM %s as t", + properties.getDatabaseContentTableName())); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + List result = new ArrayList<>(); + + Integer id; + String location; + Integer provider; + Integer secret; + + try { + while (resultSet.next()) { + id = resultSet.getInt("id"); + location = resultSet.getString("location"); + provider = resultSet.getInt("provider"); + secret = resultSet.getInt("secret"); + + result.add(ContentEntity.of(id, location, provider, secret)); + } + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return result; + } + + /** + * Deletes all entities with the given secret from content table. + * + * @param secret given secret, which allows content retrieval. + * @throws RepositoryOperationFailureException if operation execution fails. + */ + public void deleteBySecret(Integer secret) throws RepositoryOperationFailureException { + try { + repositoryExecutor.performQuery( + String.format( + "DELETE FROM %s as t WHERE t.secret = %d", + properties.getDatabaseContentTableName(), + secret)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/ProviderRepository.java b/api-server/src/main/java/com/repoachiever/repository/ProviderRepository.java new file mode 100644 index 0000000..8ab911c --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/ProviderRepository.java @@ -0,0 +1,153 @@ +package com.repoachiever.repository; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.entity.repository.ConfigEntity; +import com.repoachiever.entity.repository.ProviderEntity; +import com.repoachiever.exception.QueryEmptyResultException; +import com.repoachiever.exception.QueryExecutionFailureException; +import com.repoachiever.exception.RepositoryOperationFailureException; +import com.repoachiever.repository.executor.RepositoryExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Represents repository implementation to handle provider table. + */ +@ApplicationScoped +public class ProviderRepository { + @Inject + PropertiesEntity properties; + + @Inject + RepositoryExecutor repositoryExecutor; + + /** + * Inserts given values into the provider table. + * + * @param name given provider name. + * @throws RepositoryOperationFailureException if operation execution fails. + */ + public void insert(String name) throws RepositoryOperationFailureException { + try { + repositoryExecutor.performQuery( + String.format( + "INSERT INTO %s (name) VALUES ('%s')", + properties.getDatabaseProviderTableName(), + name)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } + + /** + * Checks if provider entity with the given name is present. + * + * @param name given name of the provider. + * @return result of the check. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public Boolean isPresentByName(String name) throws RepositoryOperationFailureException { + try { + ResultSet resultSet = repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id FROM %s as t WHERE t.name = '%s'", + properties.getDatabaseProviderTableName(), + name)); + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } catch (QueryEmptyResultException e) { + return false; + } catch (QueryExecutionFailureException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return true; + } + + /** + * Attempts to retrieve provider entity by the given name. + * + * @param name given name of the configuration. + * @return retrieved config entity. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public ProviderEntity findByName(String name) throws RepositoryOperationFailureException { + ResultSet resultSet; + + try { + resultSet = + repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.id FROM %s as t WHERE t.name = '%s'", + properties.getDatabaseProviderTableName(), + name)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + Integer id; + + try { + id = resultSet.getInt("id"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return ProviderEntity.of(id, name); + } + + /** + * Attempts to retrieve provider entity by the given identificator. + * + * @param id given identificator of the configuration. + * @return retrieved config entity. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public ProviderEntity findById(Integer id) throws RepositoryOperationFailureException { + ResultSet resultSet; + + try { + resultSet = + repositoryExecutor.performQueryWithResult( + String.format( + "SELECT t.name FROM %s as t WHERE t.id = '%s'", + properties.getDatabaseProviderTableName(), + id)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + String name; + + try { + name = resultSet.getString("name"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return ProviderEntity.of(id, name); + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/SecretRepository.java b/api-server/src/main/java/com/repoachiever/repository/SecretRepository.java new file mode 100644 index 0000000..9448bd9 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/SecretRepository.java @@ -0,0 +1,194 @@ +package com.repoachiever.repository; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.entity.repository.ProviderEntity; +import com.repoachiever.entity.repository.SecretEntity; +import com.repoachiever.exception.QueryEmptyResultException; +import com.repoachiever.exception.QueryExecutionFailureException; +import com.repoachiever.exception.RepositoryOperationFailureException; +import com.repoachiever.repository.executor.RepositoryExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.Optional; + +/** + * Represents repository implementation to handle secret table. + */ +@ApplicationScoped +public class SecretRepository { + @Inject + PropertiesEntity properties; + + @Inject + RepositoryExecutor repositoryExecutor; + + /** + * Inserts given values into the provider table. + * + * @param session given internal secret. + * @param credentials given optional external credentials. + * @throws RepositoryOperationFailureException if operation execution fails. + */ + public void insert(Integer session, Optional credentials) throws RepositoryOperationFailureException { + String query; + + if (credentials.isPresent()) { + query = String.format( + "INSERT INTO %s (session, credentials) VALUES (%d, '%s')", + properties.getDatabaseSecretTableName(), + session, + credentials.get()); + } else { + query = String.format( + "INSERT INTO %s (session) VALUES (%d)", + properties.getDatabaseSecretTableName(), + session); + } + + try { + repositoryExecutor.performQuery(query); + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } + + /** + * Checks if secret entity with the given session and credentials is present. + * + * @param session given session of the secrets set. + * @param credentials given optional external credentials. + * @return result of the check. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public Boolean isPresentBySessionAndCredentials(Integer session, Optional credentials) throws RepositoryOperationFailureException { + String query; + + if (credentials.isPresent()) { + query = String.format( + "SELECT t.id FROM %s as t WHERE t.session = %d AND t.credentials = '%s'", + properties.getDatabaseSecretTableName(), + session, + credentials.get()); + } else { + query = String.format( + "SELECT t.id FROM %s as t WHERE t.session = %d", + properties.getDatabaseSecretTableName(), + session); + } + + try { + ResultSet resultSet = repositoryExecutor.performQueryWithResult(query); + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + } catch (QueryEmptyResultException e) { + return false; + } catch (QueryExecutionFailureException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return true; + } + + /** + * Attempts to retrieve secret entity by the given session and credentials. + * + * @param session given session of the secrets set. + * @param credentials given optional external credentials. + * @return retrieved secret entity. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public SecretEntity findBySessionAndCredentials(Integer session, Optional credentials) throws RepositoryOperationFailureException { + String query; + + if (credentials.isPresent()) { + query = String.format( + "SELECT t.id FROM %s as t WHERE t.session = %d AND t.credentials = '%s'", + properties.getDatabaseSecretTableName(), + session, + credentials.get()); + } else { + query = String.format( + "SELECT t.id FROM %s as t WHERE t.session = %d", + properties.getDatabaseSecretTableName(), + session); + } + + ResultSet resultSet; + + try { + resultSet = repositoryExecutor.performQueryWithResult(query); + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + Integer id; + + try { + id = resultSet.getInt("id"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return SecretEntity.of(id, session, credentials); + } + + /** + * Attempts to retrieve secret entity by the given identificator. + * + * @param id given identificator of the secrets set. + * @return retrieved secret entity. + * @throws RepositoryOperationFailureException if repository operation fails. + */ + public SecretEntity findById(Integer id) throws RepositoryOperationFailureException { + ResultSet resultSet; + + try { + resultSet = repositoryExecutor.performQueryWithResult(String.format( + "SELECT t.session, t.credentials FROM %s as t WHERE t.id = %d", + properties.getDatabaseSecretTableName(), + id)); + + } catch (QueryExecutionFailureException | QueryEmptyResultException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + Integer session; + + try { + session = resultSet.getInt("session"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + String credentials; + + try { + credentials = resultSet.getString("credentials"); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + try { + resultSet.close(); + } catch (SQLException e) { + throw new RepositoryOperationFailureException(e.getMessage()); + } + + return SecretEntity.of(id, session, Optional.ofNullable(credentials)); + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/common/RepositoryConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/repository/common/RepositoryConfigurationHelper.java new file mode 100644 index 0000000..0112a8e --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/common/RepositoryConfigurationHelper.java @@ -0,0 +1,57 @@ +package com.repoachiever.repository.common; + +import com.repoachiever.model.CredentialsFieldsExternal; +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.CredentialsFieldsInternal; +import com.repoachiever.model.Provider; +import java.util.Optional; + +/** + * Contains helpful tools used for repository configuration. + */ +public class RepositoryConfigurationHelper { + /** + * Extracts external credentials from the given credentials field as optional . + * + * @param provider given vendor provider. + * @param credentialsFieldExternal given credentials field. + * @return extracted external credentials as optional. + */ + public static Optional getExternalCredentials( + Provider provider, CredentialsFieldsExternal credentialsFieldExternal) { + return switch (provider) { + case LOCAL -> Optional.empty(); + case GITHUB -> Optional.ofNullable(credentialsFieldExternal.getToken()); + }; + } + + /** + * Converts given raw provider to content provider. + * + * @param value given raw provider. + * @return converted content provider. + */ + public static Provider convertRawProviderToContentProvider(String value) { + return Provider.fromString(value); + } + + /** + * Converts given raw secrets to common credentials according to the given provider. + * + * @param provider given provider. + * @param session given session identificator. + * @param credentials given raw credentials. + * @return converted common credentials. + */ + public static CredentialsFieldsFull convertRawSecretsToContentCredentials( + Provider provider, Integer session, Optional credentials) { + return switch (provider) { + case LOCAL -> CredentialsFieldsFull.of( + CredentialsFieldsInternal.of(session), + null); + case GITHUB -> CredentialsFieldsFull.of( + CredentialsFieldsInternal.of(session), + CredentialsFieldsExternal.of(credentials.get())); + }; + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/executor/RepositoryExecutor.java b/api-server/src/main/java/com/repoachiever/repository/executor/RepositoryExecutor.java new file mode 100644 index 0000000..d818419 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/executor/RepositoryExecutor.java @@ -0,0 +1,154 @@ +package com.repoachiever.repository.executor; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.QueryEmptyResultException; +import com.repoachiever.exception.QueryExecutionFailureException; +import com.repoachiever.service.cluster.resource.ClusterCommunicationResource; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Service used to perform low-level database related operations. + */ +@ApplicationScoped +public class RepositoryExecutor { + private static final Logger logger = LogManager.getLogger(ClusterCommunicationResource.class); + + @Inject + PropertiesEntity properties; + + @Inject + DataSource dataSource; + + private Connection connection; + + private final List statements = new ArrayList<>(); + + private final ScheduledExecutorService scheduledExecutorService = + Executors.newSingleThreadScheduledExecutor(); + + @PostConstruct + public void configure() { + try { + this.connection = dataSource.getConnection(); + } catch (SQLException e) { + logger.fatal(new QueryExecutionFailureException(e.getMessage()).getMessage()); + } + } + + /** + * Performs given SQL query without result. + * + * @param query given SQL query to be executed. + * @throws QueryExecutionFailureException if query execution is interrupted by failure. + * @throws QueryEmptyResultException if result is empty. + */ + public void performQuery(String query) throws QueryExecutionFailureException, QueryEmptyResultException { + Statement statement; + + try { + statement = this.connection.createStatement(); + } catch (SQLException e) { + throw new QueryExecutionFailureException(e.getMessage()); + } + + try { + statement.executeUpdate(query); + } catch (SQLException e) { + throw new QueryExecutionFailureException(e.getMessage()); + } + + statements.add(statement); + + scheduledExecutorService.schedule(() -> { + try { + statement.close(); + } catch (SQLException e) { + logger.fatal(new QueryExecutionFailureException(e.getMessage()).getMessage()); + } + }, properties.getDatabaseStatementCloseDelay(), TimeUnit.MILLISECONDS); + } + + /** + * Performs given SQL query and returns raw result. + * + * @param query given SQL query to be executed. + * @return retrieved raw result. + * @throws QueryExecutionFailureException if query execution is interrupted by failure. + * @throws QueryEmptyResultException if result is empty. + */ + public ResultSet performQueryWithResult(String query) throws QueryExecutionFailureException, QueryEmptyResultException { + Statement statement; + + try { + statement = this.connection.createStatement(); + } catch (SQLException e) { + throw new QueryExecutionFailureException(e.getMessage()); + } + + ResultSet resultSet; + + try { + resultSet = statement.executeQuery(query); + } catch (SQLException e) { + throw new QueryExecutionFailureException(e.getMessage()); + } + + statements.add(statement); + + scheduledExecutorService.schedule(() -> { + try { + statement.close(); + } catch (SQLException e) { + logger.fatal(new QueryExecutionFailureException(e.getMessage()).getMessage()); + } + }, properties.getDatabaseStatementCloseDelay(), TimeUnit.MILLISECONDS); + + try { + if (!resultSet.isBeforeFirst()) { + throw new QueryEmptyResultException(); + } + } catch (SQLException e) { + throw new QueryExecutionFailureException(e.getMessage()); + } + + return resultSet; + } + + /** + * Closes opened database connection. + */ + @PreDestroy + private void close() { + statements.forEach(element -> { + try { + if (!element.isClosed()) { + element.close(); + } + } catch (SQLException e) { + logger.fatal(new QueryExecutionFailureException(e.getMessage()).getMessage()); + } + }); + + try { + this.connection.close(); + } catch (SQLException e) { + logger.fatal(e.getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/repository/facade/RepositoryFacade.java b/api-server/src/main/java/com/repoachiever/repository/facade/RepositoryFacade.java new file mode 100644 index 0000000..a77ec68 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/repository/facade/RepositoryFacade.java @@ -0,0 +1,212 @@ +package com.repoachiever.repository.facade; + +import com.repoachiever.dto.RepositoryContentUnitDto; +import com.repoachiever.entity.repository.ContentEntity; +import com.repoachiever.entity.repository.ProviderEntity; +import com.repoachiever.entity.repository.SecretEntity; +import com.repoachiever.exception.ContentApplicationRetrievalFailureException; +import com.repoachiever.exception.RepositoryContentApplicationFailureException; +import com.repoachiever.exception.RepositoryContentDestructionFailureException; +import com.repoachiever.exception.RepositoryOperationFailureException; +import com.repoachiever.model.*; +import com.repoachiever.repository.ConfigRepository; +import com.repoachiever.repository.ContentRepository; +import com.repoachiever.repository.ProviderRepository; +import com.repoachiever.repository.SecretRepository; +import com.repoachiever.repository.common.RepositoryConfigurationHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.groupingBy; + +/** + * Represents facade for repository implementations used to handle tables. + */ +@ApplicationScoped +public class RepositoryFacade { + @Inject + ConfigRepository configRepository; + + @Inject + ContentRepository contentRepository; + + @Inject + ProviderRepository providerRepository; + + @Inject + SecretRepository secretRepository; + + /** + * Retrieves content state for the given configuration properties. + * + * @param contentStateApplication given content state retrieve application. + * @return retrieve content state hash. + */ + public String retrieveContentState(ContentStateApplication contentStateApplication) { + return ""; + } + + /** + * Retrieves all the available locations for the given configuration properties. + * + * @param contentRetrievalApplication given content retrieval application. + * @return retrieved locations for the given configuration properties. + */ + public List retrieveLocations(ContentRetrievalApplication contentRetrievalApplication) { + return null; + } + + /** + * Retrieves all the data from content repository in a form of content applications. + * + * @return retrieved set of content applications. + * @throws ContentApplicationRetrievalFailureException if content application retrieval fails. + */ + public List retrieveContentApplication() throws ContentApplicationRetrievalFailureException { + List result = new ArrayList<>(); + + List units = new ArrayList<>(); + + List contents; + + try { + contents = contentRepository.findAll(); + } catch (RepositoryOperationFailureException e) { + throw new ContentApplicationRetrievalFailureException(e.getMessage()); + } + + for (ContentEntity content : contents) { + ProviderEntity rawProvider; + + try { + rawProvider = providerRepository.findById(content.getProvider()); + } catch (RepositoryOperationFailureException e) { + throw new ContentApplicationRetrievalFailureException(e.getMessage()); + } + + SecretEntity rawSecret; + + try { + rawSecret = secretRepository.findById(content.getSecret()); + } catch (RepositoryOperationFailureException e) { + throw new ContentApplicationRetrievalFailureException(e.getMessage()); + } + + Provider provider = + RepositoryConfigurationHelper.convertRawProviderToContentProvider( + rawProvider.getName()); + + CredentialsFieldsFull credentials = + RepositoryConfigurationHelper.convertRawSecretsToContentCredentials( + provider, rawSecret.getSession(), rawSecret.getCredentials()); + + units.add(RepositoryContentUnitDto.of( + content.getLocation(), + provider, + credentials)); + } + + Map>> groups = + units + .stream() + .collect( + groupingBy( + RepositoryContentUnitDto::getCredentials, + groupingBy(RepositoryContentUnitDto::getProvider))); + + groups + .forEach((key1, value1) -> { + value1 + .forEach((key2, value2) -> { + result.add( + ContentApplication.of(value2.stream().map(RepositoryContentUnitDto::getLocation).toList(), key2, key1)); + }); + }); + + return result; + } + + /** + * Applies given content application, updating previous state. + * + * @param contentApplication given content application used for topology configuration. + * @throws RepositoryContentApplicationFailureException if RepoAchiever Cluster repository content application failed. + */ + public void apply(ContentApplication contentApplication) throws RepositoryContentApplicationFailureException { + ProviderEntity provider; + + try { + provider = providerRepository.findByName(contentApplication.getProvider().toString()); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentApplicationFailureException(e.getMessage()); + } + + Optional credentials = RepositoryConfigurationHelper.getExternalCredentials( + contentApplication.getProvider(), contentApplication.getCredentials().getExternal()); + + try { + if (!secretRepository.isPresentBySessionAndCredentials( + contentApplication.getCredentials().getInternal().getId(), credentials)) { + secretRepository.insert( + contentApplication.getCredentials().getInternal().getId(), + credentials); + } + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentApplicationFailureException(e.getMessage()); + } + + SecretEntity secret; + + try { + secret = secretRepository.findBySessionAndCredentials( + contentApplication.getCredentials().getInternal().getId(), + credentials); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentApplicationFailureException(e.getMessage()); + } + + try { + contentRepository.deleteBySecret(secret.getId()); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentApplicationFailureException(e.getMessage()); + } + + for (String location : contentApplication.getLocations()) { + try { + contentRepository.insert(location, provider.getId(), secret.getId()); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentApplicationFailureException(e.getMessage()); + } + } + } + + /** + * Applies given content withdrawal, removing previous state. + * + * @param contentWithdrawal given content application used for topology configuration. + * @throws RepositoryContentDestructionFailureException if RepoAchiever Cluster repository content destruction failed. + */ + public void destroy(ContentWithdrawal contentWithdrawal) throws RepositoryContentDestructionFailureException { + Optional credentials = RepositoryConfigurationHelper.getExternalCredentials( + contentWithdrawal.getProvider(), contentWithdrawal.getCredentials().getExternal()); + + SecretEntity secret; + + try { + secret = secretRepository.findBySessionAndCredentials( + contentWithdrawal.getCredentials().getInternal().getId(), + credentials); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentDestructionFailureException(e.getMessage()); + } + + try { + contentRepository.deleteBySecret(secret.getId()); + } catch (RepositoryOperationFailureException e) { + throw new RepositoryContentDestructionFailureException(e.getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/ContentResource.java b/api-server/src/main/java/com/repoachiever/resource/ContentResource.java new file mode 100644 index 0000000..01019b5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/ContentResource.java @@ -0,0 +1,128 @@ +package com.repoachiever.resource; + +import com.repoachiever.api.ContentResourceApi; +import com.repoachiever.exception.CredentialsAreNotValidException; +import com.repoachiever.exception.CredentialsFieldIsNotValidException; +import com.repoachiever.exception.LocationsFieldIsNotValidException; +import com.repoachiever.model.*; +import com.repoachiever.repository.facade.RepositoryFacade; +import com.repoachiever.resource.common.ResourceConfigurationHelper; +import com.repoachiever.service.cluster.facade.ClusterFacade; +import com.repoachiever.service.vendor.VendorFacade; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.BadRequestException; +import lombok.SneakyThrows; + +import java.io.File; +import java.util.Objects; + +/** Contains implementation of ContentResource. */ +@ApplicationScoped +public class ContentResource implements ContentResourceApi { + @Inject + RepositoryFacade repositoryFacade; + + @Inject + ClusterFacade clusterFacade; + + @Inject + VendorFacade vendorFacade; + + /** + * Implementation for declared in OpenAPI configuration v1ContentPost method. + * + * @param contentRetrievalApplication content retrieval application. + * @return retrieved content result. + */ + @Override + public ContentRetrievalResult v1ContentPost(ContentRetrievalApplication contentRetrievalApplication) { + if (Objects.isNull(contentRetrievalApplication)) { + throw new BadRequestException(); + } + + return ContentRetrievalResult.of( + repositoryFacade.retrieveLocations(contentRetrievalApplication)); + } + + /** + * Implementation for declared in OpenAPI configuration v1ContentApplyPost method. + * + * @param contentApplication content configuration application. + */ + @Override + @SneakyThrows + public void v1ContentApplyPost(ContentApplication contentApplication) { + if (Objects.isNull(contentApplication)) { + throw new BadRequestException(); + } + + if (!ResourceConfigurationHelper.isExternalCredentialsFieldValid( + contentApplication.getProvider(), contentApplication.getCredentials().getExternal())) { + throw new CredentialsFieldIsNotValidException(); + } + + if (!ResourceConfigurationHelper.isLocationsDuplicate(contentApplication.getLocations())) { + throw new LocationsFieldIsNotValidException(); + } + + if (!vendorFacade.isExternalCredentialsValid( + contentApplication.getProvider(), contentApplication.getCredentials().getExternal())) { + throw new CredentialsAreNotValidException(); + } + + clusterFacade.apply(contentApplication); + + repositoryFacade.apply(contentApplication); + } + + /** + * Implementation for declared in OpenAPI configuration v1ContentWithdrawDelete method. + * + * @param contentWithdrawal content withdrawal application. + */ + @Override + @SneakyThrows + public void v1ContentWithdrawDelete(ContentWithdrawal contentWithdrawal) { + if (Objects.isNull(contentWithdrawal)) { + throw new BadRequestException(); + } + + if (!ResourceConfigurationHelper.isExternalCredentialsFieldValid( + contentWithdrawal.getProvider(), contentWithdrawal.getCredentials().getExternal())) { + throw new CredentialsFieldIsNotValidException(); + } + + if (!vendorFacade.isExternalCredentialsValid( + contentWithdrawal.getProvider(), contentWithdrawal.getCredentials().getExternal())) { + throw new CredentialsAreNotValidException(); + } + + clusterFacade.destroy(contentWithdrawal); + + repositoryFacade.destroy(contentWithdrawal); + } + + /** + * Implementation for declared in OpenAPI configuration v1ContentDownloadGet method. + * + * @param location name of content location to be downloaded. + * @return downloaded content result. + */ + @Override + public File v1ContentDownloadGet(String location) { + + return null; + } + + /** + * Implementation for declared in OpenAPI configuration v1ContentCleanPost method. + * + * @param contentCleanup content cleanup application. + */ + @Override + public void v1ContentCleanPost(ContentCleanup contentCleanup) { + + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/HealthResource.java b/api-server/src/main/java/com/repoachiever/resource/HealthResource.java new file mode 100644 index 0000000..30b0666 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/HealthResource.java @@ -0,0 +1,65 @@ +package com.repoachiever.resource; + +import com.repoachiever.api.HealthResourceApi; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.model.ReadinessCheckApplication; +import com.repoachiever.model.ReadinessCheckResult; +import com.repoachiever.service.client.smallrye.ISmallRyeHealthCheckClientService; +import com.repoachiever.service.workspace.WorkspaceService; +import com.repoachiever.service.workspace.facade.WorkspaceFacade; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.WebApplicationException; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.util.Objects; + +/** + * Contains implementation of HealthResource. + */ +@ApplicationScoped +public class HealthResource implements HealthResourceApi { + @Inject + PropertiesEntity properties; + + @Inject + WorkspaceFacade workspaceFacade; + + @Inject + WorkspaceService workspaceService; + + @Inject + @RestClient + ISmallRyeHealthCheckClientService smallRyeHealthCheckClientService; + + /** + * Implementation for declared in OpenAPI configuration v1HealthGet method. + * + * @return health check result. + */ + @Override + public HealthCheckResult v1HealthGet() { + try { + return smallRyeHealthCheckClientService.qHealthGet(); + } catch (WebApplicationException e) { + return e.getResponse().readEntity(HealthCheckResult.class); + } + } + + /** + * Implementation for declared in OpenAPI configuration v1ReadinessPost method. + * + * @param readinessCheckApplication application used to perform application readiness check. + * @return readiness check result. + */ + @Override + public ReadinessCheckResult v1ReadinessPost(ReadinessCheckApplication readinessCheckApplication) { + if (Objects.isNull(readinessCheckApplication)) { + throw new BadRequestException(); + } + + return null; + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/InfoResource.java b/api-server/src/main/java/com/repoachiever/resource/InfoResource.java new file mode 100644 index 0000000..a648576 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/InfoResource.java @@ -0,0 +1,56 @@ +package com.repoachiever.resource; + +import com.repoachiever.api.InfoResourceApi; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.model.ClusterInfoUnit; +import com.repoachiever.model.VersionExternalApiInfoResult; +import com.repoachiever.model.VersionInfoResult; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.List; + +/** + * Contains implementation of InfoResource. + */ +@ApplicationScoped +public class InfoResource implements InfoResourceApi { + @Inject + PropertiesEntity properties; + + /** + * Implementation for declared in OpenAPI configuration v1InfoVersionGet method. + * + * @return version information result. + */ + @Override + public VersionInfoResult v1InfoVersionGet() { + return VersionInfoResult.of( + VersionExternalApiInfoResult.of( + properties.getApplicationVersion(), properties.getGitCommitId())); + } + + /** + * Implementation for declared in OpenAPI configuration v1InfoClusterGet method. + * + * @return cluster information result. + */ + @Override + public List v1InfoClusterGet() { + // TODO: call cluster service to retrieve data from clusters. + + return null; + } + + /** + * Implementation for declared in OpenAPI configuration v1InfoTelemetryGet method. + * + * @return telemetry information result. + */ + @Override + public String v1InfoTelemetryGet() { + // TODO: call telemetry service to retrieve data. + + return null; + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/StateResource.java b/api-server/src/main/java/com/repoachiever/resource/StateResource.java new file mode 100644 index 0000000..eff6f61 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/StateResource.java @@ -0,0 +1,28 @@ +package com.repoachiever.resource; + +import com.repoachiever.api.StateResourceApi; +import com.repoachiever.model.ContentStateApplication; +import com.repoachiever.model.ContentStateApplicationResult; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.BadRequestException; + +import java.util.Objects; + +/** Contains implementation of StateResource. */ +@ApplicationScoped +public class StateResource implements StateResourceApi { + /** + * Implementation for declared in OpenAPI configuration v1StateContentPost method. + * + * @param contentStateApplication application used to perform content state retrieval. + * @return retrieved state content hash. + */ + @Override + public ContentStateApplicationResult v1StateContentPost(ContentStateApplication contentStateApplication) { + if (Objects.isNull(contentStateApplication)) { + throw new BadRequestException(); + } + + return null; + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/common/ResourceConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/resource/common/ResourceConfigurationHelper.java new file mode 100644 index 0000000..e5303a5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/common/ResourceConfigurationHelper.java @@ -0,0 +1,37 @@ +package com.repoachiever.resource.common; + +import com.repoachiever.model.CredentialsFieldsExternal; +import com.repoachiever.model.Provider; + +import java.util.List; +import java.util.Objects; + +/** + * Contains helpful tools used for resource configuration. + */ +public class ResourceConfigurationHelper { + /** + * Checks if the given external credentials field is valid according to the used provider. + * + * @param provider given vendor provider. + * @param credentialsFieldExternal given credentials field. + * @return result of the check. + */ + public static Boolean isExternalCredentialsFieldValid( + Provider provider, CredentialsFieldsExternal credentialsFieldExternal) { + return switch (provider) { + case LOCAL -> true; + case GITHUB -> Objects.nonNull(credentialsFieldExternal); + }; + } + + /** + * Checks if the given locations have duplicates. + * + * @param locations given locations. + * @return result of the check. + */ + public static Boolean isLocationsDuplicate(List locations) { + return locations.stream().distinct().count() == locations.size(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/resource/communication/ApiServerCommunicationResource.java b/api-server/src/main/java/com/repoachiever/resource/communication/ApiServerCommunicationResource.java new file mode 100644 index 0000000..e044d4b --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/resource/communication/ApiServerCommunicationResource.java @@ -0,0 +1,56 @@ +package com.repoachiever.resource.communication; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.service.communication.apiserver.IApiServerCommunicationService; +import com.repoachiever.service.integration.diagnostics.DiagnosticsConfigService; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.InputStream; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +/** + * Contains implementation of communication provider for RepoAchiever API Server. + */ +public class ApiServerCommunicationResource extends UnicastRemoteObject implements IApiServerCommunicationService { + private static final Logger logger = LogManager.getLogger(ApiServerCommunicationResource.class); + + private final PropertiesEntity properties; + + public ApiServerCommunicationResource(PropertiesEntity properties) throws RemoteException { + this.properties = properties; + } + + /** + * @see IApiServerCommunicationService + */ + @Override + public void performRawContentUpload(String workspaceUnitKey, InputStream content) throws RemoteException { + + } + + /** + * @see IApiServerCommunicationService + */ + @Override + public void performAdditionalContentUpload(String workspaceUnitKey, String content) throws RemoteException { + + } + + /** + * @see IApiServerCommunicationService + */ + @Override + public void performLogsTransfer(String name, String message) throws RemoteException { + logger.info(String.format("Transferred logs(instance: %s): %s", name, message)); + } + + /** + * @see IApiServerCommunicationService + */ + @Override + public Boolean retrieveHealthCheck() throws RemoteException { + return true; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/client/github/IGitHubClientService.java b/api-server/src/main/java/com/repoachiever/service/client/github/IGitHubClientService.java new file mode 100644 index 0000000..d7f0afa --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/client/github/IGitHubClientService.java @@ -0,0 +1,19 @@ +package com.repoachiever.service.client.github; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.annotations.jaxrs.HeaderParam; + +/** Represents client for GitHub remote API. */ +@RegisterRestClient(configKey = "github") +public interface IGitHubClientService { + @GET + @Path("/octocat") + @Produces(MediaType.APPLICATION_JSON) + Response getOctocat(@HeaderParam(HttpHeaders.AUTHORIZATION) String token); +} diff --git a/api-server/src/main/java/com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.java b/api-server/src/main/java/com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.java new file mode 100644 index 0000000..ece7ff1 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.java @@ -0,0 +1,18 @@ +package com.repoachiever.service.client.smallrye; + +import com.repoachiever.model.HealthCheckResult; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** Represents client for SmallRye health check endpoints. */ +@Path("/q") +@RegisterRestClient(configKey = "small-rye-health-check") +public interface ISmallRyeHealthCheckClientService { + @GET + @Path("/health") + @Produces(MediaType.APPLICATION_JSON) + HealthCheckResult qHealthGet(); +} diff --git a/api-server/src/main/java/com/repoachiever/service/cluster/ClusterService.java b/api-server/src/main/java/com/repoachiever/service/cluster/ClusterService.java new file mode 100644 index 0000000..6d2005b --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/cluster/ClusterService.java @@ -0,0 +1,174 @@ +package com.repoachiever.service.cluster; + +import com.repoachiever.dto.CommandExecutorOutputDto; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.*; +import com.repoachiever.service.cluster.common.ClusterConfigurationHelper; +import com.repoachiever.service.cluster.resource.ClusterCommunicationResource; +import com.repoachiever.service.command.cluster.deploy.ClusterDeployCommandService; +import com.repoachiever.service.command.cluster.destroy.ClusterDestroyCommandService; +import com.repoachiever.service.executor.CommandExecutorService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Service used for cluster deployment management, including distribution process. + */ +@ApplicationScoped +public class ClusterService { + @Inject + PropertiesEntity properties; + + @Inject + ClusterCommunicationResource clusterCommunicationResource; + + @Inject + CommandExecutorService commandExecutorService; + + /** + * Perform segregation of the given content locations according to the given segregation limitations. + * + * @param locations given content locations. + * @param separator given content location segregation separator. + * @return segregated content locations. + */ + public List> performContentLocationsSegregation(List locations, Integer separator) { + List> result = new ArrayList<>(); + + List temp = new ArrayList<>(); + + Integer counter = 0; + + for (Integer i = 0; i < locations.size(); i++) { + temp.add(locations.get(0)); + + if (counter.equals(separator - 1)) { + result.add(new ArrayList<>(temp)); + + temp.clear(); + + counter = 0; + } else { + counter++; + } + } + + if (!temp.isEmpty()) { + result.add(new ArrayList<>(temp)); + } + + return result; + } + + /** + * Performs deployment of RepoAchiever Cluster allocation. + * + * @param name given RepoAchiever Cluster allocation name. + * @param clusterContext given RepoAchiever Cluster context. + * @return process identificator of the deployed RepoAchiever Cluster instance. + * @throws ClusterDeploymentFailureException if deployment operation failed. + */ + public Integer deploy(String name, String clusterContext) throws ClusterDeploymentFailureException { + ClusterDeployCommandService clusterDeployCommandService = + new ClusterDeployCommandService( + clusterContext, + properties.getBinDirectory(), + properties.getBinClusterLocation()); + + CommandExecutorOutputDto clusterDeployCommandOutput; + + try { + clusterDeployCommandOutput = + commandExecutorService.executeCommand(clusterDeployCommandService); + } catch (CommandExecutorException e) { + throw new ClusterDeploymentFailureException(e.getMessage()); + } + + String clusterDeployCommandErrorOutput = clusterDeployCommandOutput.getErrorOutput(); + + if (Objects.nonNull(clusterDeployCommandErrorOutput) && !clusterDeployCommandErrorOutput.isEmpty()) { + throw new ClusterDeploymentFailureException(); + } + + Integer result = Integer.parseInt( + clusterDeployCommandOutput. + getNormalOutput(). + replaceAll("\n", "")); + + if (!ClusterConfigurationHelper.waitForStart(() -> { + try { + if (clusterCommunicationResource.retrieveHealthCheck(name)) { + return true; + } + } catch (ClusterOperationFailureException e) { + return false; + } + + return false; + }, + properties.getCommunicationClusterStartupAwaitFrequency(), + properties.getCommunicationClusterStartupTimeout())) { + throw new ClusterDeploymentFailureException(new ClusterApplicationTimeoutException().getMessage()); + } + + return result; + } + + /** + * Performs destruction of RepoAchiever Cluster allocation. + * + * @param pid given RepoAchiever Cluster allocation process id. + * @throws ClusterDestructionFailureException if destruction operation failed. + */ + public void destroy(Integer pid) throws ClusterDestructionFailureException { + ClusterDestroyCommandService clusterDestroyCommandService = new ClusterDestroyCommandService(pid); + + CommandExecutorOutputDto clusterDestroyCommandOutput; + + try { + clusterDestroyCommandOutput = + commandExecutorService.executeCommand(clusterDestroyCommandService); + } catch (CommandExecutorException e) { + throw new ClusterDestructionFailureException(e.getMessage()); + } + + String clusterDestroyCommandErrorOutput = clusterDestroyCommandOutput.getErrorOutput(); + + if (Objects.nonNull(clusterDestroyCommandErrorOutput) && !clusterDestroyCommandErrorOutput.isEmpty()) { + throw new ClusterDestructionFailureException(); + } + } + + /** + * Performs recreation of RepoAchiever Cluster allocation. + * + * @param pid given process identificator of the allocation RepoAchiever Cluster to be removed. + * @param name given RepoAchiever Cluster allocation name. + * @param clusterContext given RepoAchiever Cluster context used for the new allocation. + * @throws ClusterRecreationFailureException if recreation operation failed. + */ + public Integer recreate(Integer pid, String name, String clusterContext) throws ClusterRecreationFailureException { + try { + destroy(pid); + } catch (ClusterDestructionFailureException e) { + throw new ClusterRecreationFailureException(e.getMessage()); + } + + try { + return deploy(name, clusterContext); + } catch (ClusterDeploymentFailureException e) { + throw new ClusterRecreationFailureException(e.getMessage()); + } + } +} + + +// TODO: make assignment of random identificators + +// TODO: probably move this logic to ClusterService + +// TODO: should regenerate topology after each location added \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/cluster/common/ClusterConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/cluster/common/ClusterConfigurationHelper.java new file mode 100644 index 0000000..08d86cd --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/cluster/common/ClusterConfigurationHelper.java @@ -0,0 +1,70 @@ +package com.repoachiever.service.cluster.common; + +import java.util.UUID; +import java.util.concurrent.*; + +/** + * Contains helpful tools used for RepoAchiever Cluster configuration. + */ +public class ClusterConfigurationHelper { + private final static ScheduledExecutorService scheduledExecutorService = + Executors.newScheduledThreadPool(2); + + /** + * Composes name for RepoAchiever Cluster using pre-defined prefix and UUID. + * + * @param prefix given name prefix. + * @return composed RepoAchiever Cluster name. + */ + public static String getName(String prefix) { + return String.format("%s-%s", prefix, UUID.randomUUID()); + } + + /** + * Waits till the given callback execution succeeds. + * + * @param callback given callback. + * @param frequency given callback execution check frequency. + * @param timeout given callback execution timeout. + * @return result of the execution. + */ + public static Boolean waitForStart(Callable callback, Integer frequency, Integer timeout) { + CountDownLatch waiter = new CountDownLatch(1); + + ScheduledFuture awaitTask = scheduledExecutorService.scheduleAtFixedRate(() -> { + try { + if (callback.call()) { + waiter.countDown(); + } + } catch (Exception ignore) { + } + }, 0, frequency, TimeUnit.MILLISECONDS); + + ScheduledFuture timeoutTask = scheduledExecutorService.schedule(() -> { + if (!awaitTask.isCancelled()) { + awaitTask.cancel(true); + + waiter.countDown(); + } + }, timeout, TimeUnit.MILLISECONDS); + + + try { + waiter.await(); + } catch (InterruptedException e) { + return false; + } + + if (!awaitTask.isCancelled()) { + awaitTask.cancel(true); + } else { + return false; + } + + if (!timeoutTask.isDone()) { + timeoutTask.cancel(true); + } + + return true; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/cluster/facade/ClusterFacade.java b/api-server/src/main/java/com/repoachiever/service/cluster/facade/ClusterFacade.java new file mode 100644 index 0000000..0a28aae --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/cluster/facade/ClusterFacade.java @@ -0,0 +1,288 @@ +package com.repoachiever.service.cluster.facade; + +import com.repoachiever.converter.ClusterContextToJsonConverter; +import com.repoachiever.converter.ContentCredentialsToClusterContextCredentialsConverter; +import com.repoachiever.converter.ContentProviderToClusterContextProviderConverter; +import com.repoachiever.dto.ClusterAllocationDto; +import com.repoachiever.entity.common.ClusterContextEntity; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.*; +import com.repoachiever.model.ContentApplication; +import com.repoachiever.model.ContentWithdrawal; +import com.repoachiever.service.cluster.ClusterService; +import com.repoachiever.service.cluster.common.ClusterConfigurationHelper; +import com.repoachiever.service.cluster.resource.ClusterCommunicationResource; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.state.StateService; +import com.repoachiever.service.workspace.facade.WorkspaceFacade; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides high-level access to RepoAchiever Cluster related operations. + */ +@ApplicationScoped +public class ClusterFacade { + private static final Logger logger = LogManager.getLogger(ClusterFacade.class); + + @Inject + PropertiesEntity properties; + + @Inject + WorkspaceFacade workspaceFacade; + + @Inject + ConfigService configService; + + @Inject + ClusterService clusterService; + + @Inject + ClusterCommunicationResource clusterCommunicationResource; + + /** + * Applies given content application, removing previous topology and deploying new one with up-to-date configuration. + * + * @param contentApplication given content application used for topology configuration. + * @throws ClusterApplicationFailureException if RepoAchiever Cluster application failed. + */ + public void apply(ContentApplication contentApplication) throws ClusterApplicationFailureException { + StateService.getTopologyStateGuard().lock(); + + String workspaceUnitKey = + workspaceFacade.createUnitKey( + contentApplication.getProvider(), contentApplication.getCredentials()); + + List suspends = new ArrayList<>(); + + for (ClusterAllocationDto clusterAllocation : StateService. + getClusterAllocationsByWorkspaceUnitKey(workspaceUnitKey)) { + logger.info( + String.format( + "Setting RepoAchiever Cluster allocation to suspend state: %s", + clusterAllocation.getName())); + + try { + clusterCommunicationResource.performSuspend(clusterAllocation.getName()); + + } catch (ClusterOperationFailureException e) { + logger.fatal(new ClusterApplicationFailureException(e.getMessage()).getMessage()); + return; + } + + suspends.add(clusterAllocation); + } + + List> segregation = clusterService.performContentLocationsSegregation( + contentApplication.getLocations(), + configService.getConfig().getResource().getCluster().getMaxWorkers()); + + List candidates = new ArrayList<>(); + + for (List locations : segregation) { + String name = ClusterConfigurationHelper.getName(properties.getCommunicationClusterBase()); + + String context = ClusterContextToJsonConverter.convert( + ClusterContextEntity.of( + ClusterContextEntity.Metadata.of(name, workspaceUnitKey), + ClusterContextEntity.Filter.of(locations), + ClusterContextEntity.Service.of( + ContentProviderToClusterContextProviderConverter.convert( + contentApplication.getProvider()), + ContentCredentialsToClusterContextCredentialsConverter.convert( + contentApplication.getProvider(), + contentApplication.getCredentials().getExternal())), + ClusterContextEntity.Communication.of( + properties.getCommunicationApiServerName(), + configService.getConfig().getCommunication().getPort()), + ClusterContextEntity.Content.of( + configService.getConfig().getContent().getFormat()), + ClusterContextEntity.Resource.of( + ClusterContextEntity.Resource.Cluster.of( + configService.getConfig().getResource().getCluster().getMaxWorkers()), + ClusterContextEntity.Resource.Worker.of( + configService.getConfig().getResource().getWorker().getFrequency())))); + + logger.info( + String.format("Deploying RepoAchiever Cluster new allocation: %s", name)); + + Integer pid; + + try { + pid = clusterService.deploy(name, context); + } catch (ClusterDeploymentFailureException e1) { + for (ClusterAllocationDto candidate : candidates) { + logger.info( + String.format("Removing RepoAchiever Cluster candidate allocation: %s", candidate.getName())); + + try { + clusterService.destroy(candidate.getPid()); + } catch (ClusterDestructionFailureException e2) { + throw new ClusterApplicationFailureException(e1.getMessage(), e2.getMessage()); + } + } + + for (ClusterAllocationDto suspended : suspends) { + logger.info( + String.format("Setting RepoAchiever Cluster suspended allocation to serve state: %s", suspended.getName())); + + try { + clusterCommunicationResource.performServe(suspended.getName()); + } catch (ClusterOperationFailureException e2) { + logger.fatal(new ClusterApplicationFailureException(e1.getMessage(), e2.getMessage()).getMessage()); + return; + } + } + + throw new ClusterApplicationFailureException(e1.getMessage()); + } + + candidates.add(ClusterAllocationDto.of(name, pid, context, workspaceUnitKey)); + } + + for (ClusterAllocationDto candidate : candidates) { + logger.info( + String.format( + "Setting RepoAchiever Cluster candidate allocation to serve state: %s", + candidate.getName())); + + try { + clusterCommunicationResource.performServe(candidate.getName()); + } catch (ClusterOperationFailureException e1) { + for (ClusterAllocationDto suspended : suspends) { + logger.info( + String.format( + "Setting RepoAchiever Cluster suspended allocation to serve state: %s", + suspended.getName())); + + try { + clusterCommunicationResource.performServe(suspended.getName()); + } catch (ClusterOperationFailureException e2) { + logger.fatal(new ClusterApplicationFailureException( + e1.getMessage(), e2.getMessage()).getMessage()); + return; + } + } + + throw new ClusterApplicationFailureException(e1.getMessage()); + } + } + + for (ClusterAllocationDto suspended : suspends) { + logger.info( + String.format("Removing RepoAchiever Cluster suspended allocation: %s", suspended.getName())); + + try { + clusterService.destroy(suspended.getPid()); + } catch (ClusterDestructionFailureException e) { + throw new ClusterApplicationFailureException(e.getMessage()); + } + } + + StateService.addClusterAllocations(candidates); + + StateService.removeClusterAllocationByNames( + suspends.stream().map(ClusterAllocationDto::getName).toList()); + + StateService.getTopologyStateGuard().unlock(); + } + + /** + * Applies given content withdrawal, removing existing content configuration with the given properties. + * + * @param contentWithdrawal given content application used for topology configuration. + * @throws ClusterWithdrawalFailureException if RepoAchiever Cluster withdrawal failed. + */ + public void destroy(ContentWithdrawal contentWithdrawal) throws ClusterWithdrawalFailureException { + StateService.getTopologyStateGuard().lock(); + + String workspaceUnitKey = + workspaceFacade.createUnitKey( + contentWithdrawal.getProvider(), contentWithdrawal.getCredentials()); + + List clusterAllocations = + StateService.getClusterAllocationsByWorkspaceUnitKey(workspaceUnitKey); + + for (ClusterAllocationDto clusterAllocation : clusterAllocations) { + logger.info( + String.format("Removing RepoAchiever Cluster allocation: %s", clusterAllocation.getName())); + + try { + clusterService.destroy(clusterAllocation.getPid()); + } catch (ClusterDestructionFailureException e) { + throw new ClusterWithdrawalFailureException(e.getMessage()); + } + } + + StateService.removeClusterAllocationByNames( + clusterAllocations.stream().map(ClusterAllocationDto::getName).toList()); + + StateService.getTopologyStateGuard().unlock(); + } + + /** + * Destroys all the created RepoAchiever Cluster allocations. + * + * @throws ClusterFullDestructionFailureException if RepoAchiever Cluster full destruction failed. + */ + public void destroyAll() throws ClusterFullDestructionFailureException { + StateService.getTopologyStateGuard().lock(); + + for (ClusterAllocationDto clusterAllocation : StateService.getClusterAllocations()) { + logger.info( + String.format("Removing RepoAchiever Cluster allocation: %s", clusterAllocation.getName())); + + try { + clusterService.destroy(clusterAllocation.getPid()); + } catch (ClusterDestructionFailureException e) { + throw new ClusterFullDestructionFailureException(e.getMessage()); + } + } + + StateService.getTopologyStateGuard().unlock(); + } + + /** + * Reapplies all unhealthy RepoAchiever Cluster allocations, which healthcheck operation failed for, recreating them. + * + * @throws ClusterUnhealthyReapplicationFailureException if RepoAchiever Cluster unhealthy allocation reapplication fails. + */ + public void reapplyUnhealthy() throws ClusterUnhealthyReapplicationFailureException { + StateService.getTopologyStateGuard().lock(); + + + List updates = new ArrayList<>(); + List removable = new ArrayList<>(); + + for (ClusterAllocationDto clusterAllocation : StateService.getClusterAllocations()) { + try { + clusterService.destroy(clusterAllocation.getPid()); + } catch (ClusterDestructionFailureException ignored) { + } + + Integer pid; + + try { + pid = clusterService.deploy(clusterAllocation.getName(), clusterAllocation.getContext()); + } catch (ClusterDeploymentFailureException e) { + throw new ClusterUnhealthyReapplicationFailureException(e.getMessage()); + } + + removable.add(clusterAllocation.getName()); +// +// updates.add(ClusterAllocationDto.of( +// clusterAllocation.getName(), pid, clusterAllocation.getContext())); + } + + StateService.removeClusterAllocationByNames(removable); + +// updates.forEach(StateService::addClusterAllocation); + + StateService.getTopologyStateGuard().unlock(); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/cluster/resource/ClusterCommunicationResource.java b/api-server/src/main/java/com/repoachiever/service/cluster/resource/ClusterCommunicationResource.java new file mode 100644 index 0000000..86c8a13 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/cluster/resource/ClusterCommunicationResource.java @@ -0,0 +1,142 @@ +package com.repoachiever.service.cluster.resource; + +import com.repoachiever.exception.ClusterOperationFailureException; +import com.repoachiever.exception.CommunicationConfigurationFailureException; +import com.repoachiever.service.communication.common.CommunicationProviderConfigurationHelper; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import com.repoachiever.service.communication.cluster.IClusterCommunicationService; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +/** + * Represents implementation for RepoAchiever Cluster remote API. + */ +@ApplicationScoped +public class ClusterCommunicationResource { + private static final Logger logger = LogManager.getLogger(ClusterCommunicationResource.class); + + @Inject + ConfigService configService; + + private Registry registry; + + @PostConstruct + private void configure() { + try { + this.registry = LocateRegistry.getRegistry( + configService.getConfig().getCommunication().getPort()); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + } + } + + /** + * Retrieves remote RepoAchiever Cluster allocation with the given name. + * + * @param name given RepoAchiever Cluster allocation name. + * @return retrieved RepoAchiever Cluster allocation. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + private IClusterCommunicationService retrieveAllocation(String name) throws ClusterOperationFailureException { + try { + return (IClusterCommunicationService) registry.lookup( + CommunicationProviderConfigurationHelper.getBindName( + configService.getConfig().getCommunication().getPort(), + name)); + } catch (RemoteException | NotBoundException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } + + /** + * Performs RepoAchiever Cluster suspend operation. Has no effect if RepoAchiever Cluster was already suspended + * previously. + * + * @param name given name of RepoAchiever Cluster. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + public void performSuspend(String name) throws ClusterOperationFailureException { + IClusterCommunicationService allocation = retrieveAllocation(name); + + try { + allocation.performSuspend(); + } catch (RemoteException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } + + /** + * Performs RepoAchiever Cluster serve operation. Has no effect if RepoAchiever Cluster was not suspended previously. + * + * @param name given name of RepoAchiever Cluster. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + public void performServe(String name) throws ClusterOperationFailureException { + IClusterCommunicationService allocation = retrieveAllocation(name); + + try { + allocation.performServe(); + } catch (RemoteException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } + + /** + * Retrieves health check status of the RepoAchiever Cluster with the given name. + * + * @param name given name of RepoAchiever Cluster. + * @return result of the check. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + public Boolean retrieveHealthCheck(String name) throws ClusterOperationFailureException { + IClusterCommunicationService allocation = retrieveAllocation(name); + + try { + return allocation.retrieveHealthCheck(); + } catch (RemoteException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } + + /** + * Retrieves version of the RepoAchiever Cluster with the given name. + * + * @param name given name of RepoAchiever Cluster. + * @return retrieved version of RepoAchiever Cluster. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + public String retrieveVersion(String name) throws ClusterOperationFailureException { + IClusterCommunicationService allocation = retrieveAllocation(name); + + try { + return allocation.retrieveVersion(); + } catch (RemoteException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } + + /** + * Retrieves amount of RepoAchiever Worker owned by RepoAchiever Cluster with the given name. + * + * @param name given name of RepoAchiever Cluster. + * @return retrieved amount of RepoAchiever Worker owned by RepoAchiever Cluster allocation. + * @throws ClusterOperationFailureException if RepoAchiever Cluster operation fails. + */ + public Integer retrieveWorkerAmount(String name) throws ClusterOperationFailureException { + IClusterCommunicationService allocation = retrieveAllocation(name); + + try { + return allocation.retrieveWorkerAmount(); + } catch (RemoteException e) { + throw new ClusterOperationFailureException(e.getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.java new file mode 100644 index 0000000..0f1fd0f --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.java @@ -0,0 +1,25 @@ +package com.repoachiever.service.command.cluster.common; + +import com.repoachiever.service.command.common.CommandConfigurationHelper; + +import java.util.HashMap; + +/** + * Contains helpful tools used for Grafana deployment configuration. + */ +public class ClusterConfigurationHelper { + /** + * Composes environment variables for Grafana deployment. + * + * @param clusterContext RepoAchiever Cluster context used for cluster configuration. + * @return composed environment variables. + */ + public static String getEnvironmentVariables(String clusterContext) { + return CommandConfigurationHelper.getEnvironmentVariables( + new HashMap<>() { + { + put("REPOACHIEVER_CLUSTER_CONTEXT", clusterContext); + } + }); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.java new file mode 100644 index 0000000..249c0a3 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.java @@ -0,0 +1,38 @@ +package com.repoachiever.service.command.cluster.deploy; + +import com.repoachiever.service.command.cluster.common.ClusterConfigurationHelper; +import process.SProcess; +import process.SProcessExecutor; + +import java.nio.file.Path; + +/** + * Represents RepoAchiever Cluster deployment command. + */ +public class ClusterDeployCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public ClusterDeployCommandService( + String clusterContext, String binDirectory, String binClusterLocation) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format( + "%s java -jar %s & echo $!", + ClusterConfigurationHelper.getEnvironmentVariables(clusterContext), + Path.of(binDirectory, binClusterLocation)); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.java new file mode 100644 index 0000000..fb45f93 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.java @@ -0,0 +1,31 @@ +package com.repoachiever.service.command.cluster.destroy; + +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents RepoAchiever Cluster destruction command. + */ +public class ClusterDestroyCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public ClusterDestroyCommandService(Integer pid) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format("kill -15 %d", pid); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/common/CommandConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/command/common/CommandConfigurationHelper.java new file mode 100644 index 0000000..ae227ba --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/common/CommandConfigurationHelper.java @@ -0,0 +1,71 @@ +package com.repoachiever.service.command.common; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Contains helpful tools used for general command configuration. + */ +public class CommandConfigurationHelper { + /** + * Composes environment variables. + * + * @param attributes attributes to be included. + * @return composed environment variables. + */ + public static String getEnvironmentVariables(Map attributes) { + return attributes.entrySet().stream() + .map(element -> String.format("%s='%s'", element.getKey(), element.getValue())) + .collect(Collectors.joining(" ")); + } + + /** + * Composes Docker volumes declaration. + * + * @param attributes attributes to be included. + * @return composed Docker volumes declaration. + */ + public static String getDockerVolumes(Map attributes) { + return attributes.entrySet().stream() + .map(element -> String.format("-v %s:%s", element.getKey(), element.getValue())) + .collect(Collectors.joining(" ")); + } + + /** + * Composes Docker command arguments declaration. + * + * @param attributes attributes to be included. + * @return composed Docker command arguments declaration. + */ + public static String getDockerCommandArguments(Map attributes) { + return attributes.entrySet().stream() + .map(element -> String.format("--%s=%s", element.getKey(), element.getValue())) + .collect(Collectors.joining(" ")); + } + + /** + * Composes Docker command options declaration. + * + * @param attributes attributes to be included. + * @return composed Docker command options declaration. + */ + public static String getDockerCommandOptions(List attributes) { + return attributes. + stream(). + map(element -> String.format("--%s", element)) + .collect(Collectors.joining(" ")); + } + + /** + * Composes Docker port mappings. + * + * @param attributes attributes to be included. + * @return composed Docker port mappings. + */ + public static String getDockerPorts(Map attributes) { + return attributes.entrySet().stream() + .map(element -> String.format("-p 0.0.0.0:%d:%d", element.getKey(), element.getValue())) + .collect(Collectors.joining(" ")); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.java new file mode 100644 index 0000000..a40d911 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.java @@ -0,0 +1,33 @@ +package com.repoachiever.service.command.docker.availability; + +import jakarta.enterprise.context.ApplicationScoped; +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Docker availability check command. + */ +@ApplicationScoped +public class DockerAvailabilityCheckCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public DockerAvailabilityCheckCommandService() { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> "docker ps 2>/dev/null"; + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.java new file mode 100644 index 0000000..a0fc855 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.java @@ -0,0 +1,32 @@ +package com.repoachiever.service.command.docker.inspect.remove; + +import jakarta.enterprise.context.ApplicationScoped; +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Docker container removal command. Does nothing if given container does not exist. + */ +public class DockerInspectRemoveCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public DockerInspectRemoveCommandService(String name) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format("docker rm -f %s 2>/dev/null", name); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.java new file mode 100644 index 0000000..1d6c3cc --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.java @@ -0,0 +1,34 @@ +package com.repoachiever.service.command.docker.network.create; + +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Docker network creation command. + */ +public class DockerNetworkCreateCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public DockerNetworkCreateCommandService(String networkName) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format( + "docker network inspect %s >/dev/null 2>&1 || docker network create -d bridge %s", + networkName, + networkName); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.java new file mode 100644 index 0000000..0f94225 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.java @@ -0,0 +1,31 @@ +package com.repoachiever.service.command.docker.network.remove; + +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Docker network removal command. + */ +public class DockerNetworkRemoveCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public DockerNetworkRemoveCommandService(String networkName) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format("docker network rm %s 2> /dev/null", networkName); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/grafana/GrafanaDeployCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/grafana/GrafanaDeployCommandService.java new file mode 100644 index 0000000..4498f9b --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/grafana/GrafanaDeployCommandService.java @@ -0,0 +1,38 @@ +package com.repoachiever.service.command.grafana; + +import com.repoachiever.service.command.grafana.common.GrafanaConfigurationHelper; +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Grafana deployment command. + */ +public class GrafanaDeployCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public GrafanaDeployCommandService( + String name, String image, Integer port, String configLocation, String internalLocation) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format( + "docker run -d %s %s --name %s %s", + GrafanaConfigurationHelper.getDockerVolumes(configLocation, internalLocation), + GrafanaConfigurationHelper.getDockerPorts(port), + name, + image); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.java new file mode 100644 index 0000000..0d203ae --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.java @@ -0,0 +1,57 @@ +package com.repoachiever.service.command.grafana.common; + +import com.repoachiever.service.command.common.CommandConfigurationHelper; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +/** Contains helpful tools used for Grafana deployment configuration. */ +public class GrafanaConfigurationHelper { + /** + * Composes environment variables for Grafana deployment. + * + * @return composed environment variables. + */ + public static String getEnvironmentVariables() { + return CommandConfigurationHelper.getEnvironmentVariables( + new HashMap<>() { + { + put("GF_SECURITY_ADMIN_PASSWORD", "repoachiever"); + put("GF_USERS_ALLOW_SIGN_UP", "false"); + } + }); + } + + /** + * Composes Grafana Docker volumes declaration. + * + * @param configLocation given Grafana local config directory location. + * @param internalLocation given Grafana local internal directory location. + * @return composed Grafana Docker volumes declaration. + */ + public static String getDockerVolumes(String configLocation, String internalLocation) { + return CommandConfigurationHelper.getDockerVolumes( + new HashMap<>() { + { + put(configLocation, "/etc/grafana/provisioning/"); + put(internalLocation, "/var/lib/grafana"); + } + }); + } + + /** + * Composes Prometheus Docker port mappings. + * + * @param port given Prometheus Docker port. + * @return composed Prometheus Docker port mappings. + */ + public static String getDockerPorts(Integer port) { + return CommandConfigurationHelper.getDockerPorts( + new HashMap<>() { + { + put(port, 3000); + } + }); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.java new file mode 100644 index 0000000..de20bc5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.java @@ -0,0 +1,38 @@ +package com.repoachiever.service.command.nodeexporter; + +import com.repoachiever.service.command.nodeexporter.common.NodeExporterConfigurationHelper; +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Prometheus Node Exporter deployment command. + */ +public class NodeExporterDeployCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public NodeExporterDeployCommandService(String name, String image, Integer port) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format( + "docker run -d %s %s --name %s %s %s", + NodeExporterConfigurationHelper.getDockerVolumes(), + NodeExporterConfigurationHelper.getDockerPorts(port), + name, + image, + NodeExporterConfigurationHelper.getDockerCommandArguments()); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.java new file mode 100644 index 0000000..cbf60fc --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.java @@ -0,0 +1,60 @@ +package com.repoachiever.service.command.nodeexporter.common; + +import com.repoachiever.service.command.common.CommandConfigurationHelper; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * Contains helpful tools used for Prometheus NodeExporter deployment configuration. + */ +public class NodeExporterConfigurationHelper { + /** + * Composes Prometheus Node Exporter Docker volumes declaration. + * + * @return composed Prometheus Node Exporter Docker volumes declaration. + */ + public static String getDockerVolumes() { + return CommandConfigurationHelper.getDockerVolumes( + new HashMap<>() { + { + put("/proc", "/host/proc:ro"); + put("/sys", "/host/sys:ro"); + put("/", "/rootfs:ro"); + } + }); + } + + /** + * Composes Prometheus Node Exporter Docker command arguments declaration. + * + * @return composed Prometheus Node Exporter Docker command arguments declaration. + */ + public static String getDockerCommandArguments() { + return CommandConfigurationHelper.getDockerCommandArguments( + new LinkedHashMap<>() { + { + put("path.procfs", "/host/proc"); + put("path.sysfs", "/host/sys"); + put("collector.filesystem.ignored-mount-points", "\"^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)\""); + } + }); + } + + + /** + * Composes Prometheus Node Exporter Docker port mappings. + * + * @param port given Prometheus Node Exporter Docker port. + * @return composed Prometheus Node Exporter Docker port mappings. + */ + public static String getDockerPorts(Integer port) { + return CommandConfigurationHelper.getDockerPorts( + new HashMap<>() { + { + put(port, port); + } + }); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.java b/api-server/src/main/java/com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.java new file mode 100644 index 0000000..c835878 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.java @@ -0,0 +1,56 @@ +package com.repoachiever.service.command.prometheus; + +import com.repoachiever.service.command.prometheus.common.PrometheusConfigurationHelper; +import process.SProcess; +import process.SProcessExecutor; + +/** + * Represents Prometheus deployment command. + */ +public class PrometheusDeployCommandService extends SProcess { + private final String command; + private final SProcessExecutor.OS osType; + + public PrometheusDeployCommandService( + String name, String image, Integer port, String configLocation, String internalLocation) { + this.osType = SProcessExecutor.getCommandExecutor().getOSType(); + + this.command = switch (osType) { + case WINDOWS -> null; + case UNIX, MAC, ANY -> String.format( + "docker run -d %s %s %s --name %s %s %s %s", + PrometheusConfigurationHelper.getDockerParameters(), + PrometheusConfigurationHelper.getDockerVolumes(configLocation, internalLocation), + PrometheusConfigurationHelper.getDockerPorts(port), + name, + image, + PrometheusConfigurationHelper.getDockerCommandArguments(), + PrometheusConfigurationHelper.getDockerCommandOptions()); + }; + } + + @Override + public String getCommand() { + return command; + } + + @Override + public SProcessExecutor.OS getOSType() { + return osType; + } +} + +//prometheus: +// image: prom/prometheus:v2.36.2 +// volumes: +// - ./prometheus/:/etc/prometheus/ +// - prometheus_data:/prometheus +// command: +// - '--config.file=/etc/prometheus/prometheus.yml' +// - '--storage.tsdb.path=/prometheus' +// - '--web.console.libraries=/usr/share/prometheus/console_libraries' +// - '--web.console.templates=/usr/share/prometheus/consoles' +// - '--web.enable-lifecycle' +// - '--web.enable-admin-api' +// ports: +// - 9090:9090 \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.java new file mode 100644 index 0000000..85fb2c7 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.java @@ -0,0 +1,86 @@ +package com.repoachiever.service.command.prometheus.common; + +import com.repoachiever.service.command.common.CommandConfigurationHelper; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; + + +/** + * Contains helpful tools used for Prometheus deployment configuration. + */ +public class PrometheusConfigurationHelper { + /** + * Composes Prometheus Docker additional parameters. + * + * @return composed Docker additional parameters. + */ + public static String getDockerParameters() { + return "--add-host=host.docker.internal:host-gateway"; + } + + /** + * Composes Prometheus Docker volumes declaration. + * + * @param configLocation given Prometheus local config directory location. + * @param internalLocation given Prometheus local internal directory location. + * @return composed Prometheus Docker volumes declaration. + */ + public static String getDockerVolumes(String configLocation, String internalLocation) { + return CommandConfigurationHelper.getDockerVolumes( + new HashMap<>() { + { + put(configLocation, "/etc/prometheus/"); + put(internalLocation, "/prometheus"); + } + }); + } + + /** + * Composes Prometheus Docker command arguments declaration. + * + * @return composed Prometheus Docker command arguments declaration. + */ + public static String getDockerCommandArguments() { + return CommandConfigurationHelper.getDockerCommandArguments( + new LinkedHashMap<>() { + { + put("config.file", "/etc/prometheus/prometheus.yml"); + put("storage.tsdb.path", "/prometheus"); + put("web.console.libraries", "/usr/share/prometheus/console_libraries"); + put("web.console.templates", "/usr/share/prometheus/consoles"); + } + }); + } + + /** + * Composes Prometheus Docker command options declaration. + * + * @return composed Prometheus Docker command options declaration. + */ + public static String getDockerCommandOptions() { + return CommandConfigurationHelper.getDockerCommandOptions( + List.of("web.enable-lifecycle", + "web.enable-admin-api")); + } + + /** + * Composes Prometheus Docker port mappings. + * + * @param port given Prometheus Docker port. + * @return composed Prometheus Docker port mappings. + */ + public static String getDockerPorts(Integer port) { + return CommandConfigurationHelper.getDockerPorts( + new HashMap<>() { + { + put(port, port); + } + }); + } +} + + + diff --git a/api-server/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java b/api-server/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java new file mode 100644 index 0000000..e7e7a15 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java @@ -0,0 +1,45 @@ +package com.repoachiever.service.communication.apiserver; + +import java.io.InputStream; +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * Represents communication provider for RepoAchiever API Server. + */ +public interface IApiServerCommunicationService extends Remote { + /** + * Performs raw content upload operation, initiated by RepoAchiever Cluster. + * + * @param workspaceUnitKey given user workspace unit key. + * @param content given content to be uploaded. + * @throws RemoteException if remote request fails. + */ + void performRawContentUpload(String workspaceUnitKey, InputStream content) throws RemoteException; + + /** + * Performs additional content(issues, prs, releases) upload operation, initiated by RepoAchiever Cluster. + * + * @param workspaceUnitKey given user workspace unit key. + * @param content given content to be uploaded. + * @throws RemoteException if remote request fails. + */ + void performAdditionalContentUpload(String workspaceUnitKey, String content) throws RemoteException; + + /** + * Handles incoming log messages related to the given RepoAchiever Cluster allocation. + * + * @param name given RepoAchiever Cluster allocation name. + * @param message given RepoAchiever Cluster log message. + * @throws RemoteException if remote request fails. + */ + void performLogsTransfer(String name, String message) throws RemoteException; + + /** + * Retrieves latest RepoAchiever API Server health check states. + * + * @return RepoAchiever API Server health check status. + * @throws RemoteException if remote request fails. + */ + Boolean retrieveHealthCheck() throws RemoteException; +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java b/api-server/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java new file mode 100644 index 0000000..92fc69b --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java @@ -0,0 +1,50 @@ +package com.repoachiever.service.communication.cluster; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** Represents client for RepoAchiever Cluster remote API. */ +public interface IClusterCommunicationService extends Remote { + /** + * Performs RepoAchiever Cluster suspend operation. Has no effect if RepoAchiever Cluster was + * already suspended previously. + * + * @throws RemoteException if remote request fails. + */ + void performSuspend() throws RemoteException; + + /** + * Performs RepoAchiever Cluster serve operation. Has no effect if RepoAchiever Cluster was not + * suspended previously. + * + * @throws RemoteException if remote request fails. + */ + void performServe() throws RemoteException; + + /** + * Retrieves latest RepoAchiever Cluster health check states. + * + * @return RepoAchiever Cluster health check status. + * @throws RemoteException if remote request fails. + */ + Boolean retrieveHealthCheck() throws RemoteException; + + /** + * Retrieves version of the allocated RepoAchiever Cluster instance allowing to confirm API + * compatability. + * + * @return RepoAchiever Cluster version. + * @throws RemoteException if remote request fails. + */ + String retrieveVersion() throws RemoteException; + + /** + * Retrieves amount of allocated workers. + * + * @return amount of allocated workers. + * @throws RemoteException if remote request fails. + */ + Integer retrieveWorkerAmount() throws RemoteException; +} + +// TODO: LOCATE ALL RMI RELATED CLASSES AT THE SAME PATH \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java new file mode 100644 index 0000000..0a5462c --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java @@ -0,0 +1,15 @@ +package com.repoachiever.service.communication.common; + +/** Contains helpful tools used for communication provider configuration. */ +public class CommunicationProviderConfigurationHelper { + /** + * Composes binding URI declaration for RMI. + * + * @param registryPort given registry port. + * @param suffix given binding suffix. + * @return composed binding URI declaration for RMI. + */ + public static String getBindName(Integer registryPort, String suffix) { + return String.format("//localhost:%d/%s", registryPort, suffix); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/config/ConfigService.java b/api-server/src/main/java/com/repoachiever/service/config/ConfigService.java new file mode 100644 index 0000000..eced9fe --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/config/ConfigService.java @@ -0,0 +1,103 @@ +package com.repoachiever.service.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.repoachiever.entity.common.ConfigEntity; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.ConfigValidationException; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; + +import java.io.*; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Service used to perform RepoAchiever API Server configuration processing operation. + */ +@Startup +@ApplicationScoped +public class ConfigService { + private static final Logger logger = LogManager.getLogger(ConfigService.class); + + @Inject + PropertiesEntity properties; + + @Getter + private ConfigEntity config; + + /** + * Reads configuration from the opened configuration file using mapping with a configuration entity. + */ + @PostConstruct + private void configure() { + InputStream file = null; + + try { + try { + file = new FileInputStream( + Paths.get(properties.getConfigDirectory(), properties.getConfigName()).toString()); + } catch (FileNotFoundException e) { + logger.fatal(e.getMessage()); + return; + } + + ObjectMapper mapper = + new ObjectMapper(new YAMLFactory()) + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectReader reader = mapper.reader().forType(new TypeReference() { + }); + + try { + List values = reader.readValues(file).readAll(); + if (values.isEmpty()) { + return; + } + + config = values.getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + return; + } + + try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { + Validator validator = validatorFactory.getValidator(); + + Set> validationResult = + validator.validate(config); + + if (!validationResult.isEmpty()) { + logger.fatal(new ConfigValidationException( + validationResult.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", "))).getMessage()); + } + } + } finally { + try { + file.close(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java b/api-server/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java new file mode 100644 index 0000000..7dbff48 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java @@ -0,0 +1,62 @@ +package com.repoachiever.service.executor; + +import com.repoachiever.dto.CommandExecutorOutputDto; +import com.repoachiever.exception.CommandExecutorException; +import jakarta.enterprise.context.ApplicationScoped; +import process.SProcess; +import process.SProcessExecutor; +import process.exceptions.NonMatchingOSException; +import process.exceptions.SProcessNotYetStartedException; + +import java.io.IOException; + +/** + * Represents command executor service used to perform commands execution. + */ +@ApplicationScoped +public class CommandExecutorService { + private final SProcessExecutor processExecutor; + + CommandExecutorService() { + this.processExecutor = SProcessExecutor.getCommandExecutor(); + } + + /** + * Executes given command. + * + * @param command standalone command + * @return output result, which consists of stdout and stderr. + * @throws CommandExecutorException when any execution step failed. + */ + public CommandExecutorOutputDto executeCommand(SProcess command) throws CommandExecutorException { + try { + processExecutor.executeCommand(command); + } catch (IOException | NonMatchingOSException e) { + throw new CommandExecutorException(e.getMessage()); + } + + try { + command.waitForCompletion(); + } catch (SProcessNotYetStartedException | InterruptedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + String commandErrorOutput; + + try { + commandErrorOutput = command.getErrorOutput(); + } catch (SProcessNotYetStartedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + String commandNormalOutput; + + try { + commandNormalOutput = command.getNormalOutput(); + } catch (SProcessNotYetStartedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + return CommandExecutorOutputDto.of(commandNormalOutput, commandErrorOutput); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/healthcheck/health/HealthCheckService.java b/api-server/src/main/java/com/repoachiever/service/healthcheck/health/HealthCheckService.java new file mode 100644 index 0000000..b368b16 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/healthcheck/health/HealthCheckService.java @@ -0,0 +1,20 @@ +package com.repoachiever.service.healthcheck.health; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.health.*; + +@Liveness +@ApplicationScoped +public class HealthCheckService implements HealthCheck { + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder healthCheckResponse = + HealthCheckResponse.named("Terraform application availability"); + + healthCheckResponse.up(); + + // TODO: check if docker is installed + + return healthCheckResponse.build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.java b/api-server/src/main/java/com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.java new file mode 100644 index 0000000..c399e23 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.java @@ -0,0 +1,23 @@ +package com.repoachiever.service.healthcheck.readiness; + +import com.repoachiever.entity.common.PropertiesEntity; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.HealthCheckResponseBuilder; + +/** Checks if the Kafka service is available for the given workspace. */ +public class ReadinessCheckService implements HealthCheck { + + public ReadinessCheckService(PropertiesEntity properties) { + } + + @Override + public HealthCheckResponse call() { + HealthCheckResponseBuilder healthCheckResponse = + HealthCheckResponse.named("Connection availability"); + + healthCheckResponse.up(); + + return healthCheckResponse.build(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.java new file mode 100644 index 0000000..94f5bb4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.java @@ -0,0 +1,60 @@ +package com.repoachiever.service.integration.communication.apiserver; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.CommunicationConfigurationFailureException; +import com.repoachiever.resource.communication.ApiServerCommunicationResource; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.communication.common.CommunicationProviderConfigurationHelper; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +/** + * Service used to perform RepoAchiever API Server communication provider configuration. + */ +@Startup(value = 160) +@ApplicationScoped +public class ApiServerCommunicationConfigService { + private static final Logger logger = LogManager.getLogger(ApiServerCommunicationConfigService.class); + + @Inject + PropertiesEntity properties; + + @Inject + ConfigService configService; + + /** + * Performs setup of RepoAchiever API Server communication provider. + */ + @PostConstruct + private void process() { + Registry registry; + + try { + registry = LocateRegistry.getRegistry( + configService.getConfig().getCommunication().getPort()); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + return; + } + + Thread.ofPlatform().start(() -> { + try { + registry.rebind( + CommunicationProviderConfigurationHelper.getBindName( + configService.getConfig().getCommunication().getPort(), + properties.getCommunicationApiServerName()), + new ApiServerCommunicationResource(properties)); + } catch (RemoteException e) { + logger.fatal(e.getMessage()); + } + }); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.java b/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.java new file mode 100644 index 0000000..02864e4 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.java @@ -0,0 +1,58 @@ +package com.repoachiever.service.integration.communication.cluster.healthcheck; + +import com.repoachiever.dto.ClusterAllocationDto; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.*; +import com.repoachiever.resource.communication.ApiServerCommunicationResource; +import com.repoachiever.service.cluster.ClusterService; +import com.repoachiever.service.cluster.facade.ClusterFacade; +import com.repoachiever.service.communication.common.CommunicationProviderConfigurationHelper; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.state.StateService; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Service used to perform RepoAchiever Cluster communication health check operations. + */ +@ApplicationScoped +public class ClusterHealthCheckCommunicationService { + private static final Logger logger = LogManager.getLogger(ClusterHealthCheckCommunicationService.class); + + @Inject + PropertiesEntity properties; + + @Inject + ClusterFacade clusterFacade; + + private final static ScheduledExecutorService scheduledExecutorService = + Executors.newSingleThreadScheduledExecutor(); + + /** + * Performs RepoAchiever Cluster communication health check operations. If RepoAchiever Cluster is not responding, + * then it will be redeployed. + */ + @PostConstruct + private void process() { + scheduledExecutorService.schedule(() -> { +// try { +// clusterFacade.reapplyUnhealthy(); +// } catch (ClusterUnhealthyReapplicationFailureException e) { +// logger.fatal(e.getMessage()); +// } + }, properties.getCommunicationClusterHealthCheckFrequency(), TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.java new file mode 100644 index 0000000..fdcd129 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.java @@ -0,0 +1,65 @@ +package com.repoachiever.service.integration.communication.cluster.topology; + +import com.repoachiever.exception.*; +import com.repoachiever.model.ContentApplication; +import com.repoachiever.repository.facade.RepositoryFacade; +import com.repoachiever.service.cluster.facade.ClusterFacade; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.Shutdown; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; + +/** + * Service used to perform topology configuration. + */ +@Startup(value = 170) +@ApplicationScoped +public class ClusterTopologyCommunicationConfigService { + private static final Logger logger = LogManager.getLogger(ClusterTopologyCommunicationConfigService.class); + + @Inject + RepositoryFacade repositoryFacade; + + @Inject + ClusterFacade clusterFacade; + + /** + * Recreates previously created topology infrastructure if such existed before. + */ + @PostConstruct + private void process() { + List applications; + + try { + applications = repositoryFacade.retrieveContentApplication(); + } catch (ContentApplicationRetrievalFailureException ignored) { + return; + } + + for (ContentApplication application : applications) { + try { + clusterFacade.apply(application); + } catch (ClusterApplicationFailureException e) { + logger.fatal(e.getMessage()); + return; + } + } + } + + /** + * Gracefully stops all the created topology infrastructure. + */ + public void close(@Observes Shutdown event) { + try { + clusterFacade.destroyAll(); + } catch (ClusterFullDestructionFailureException e) { + logger.error(e.getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.java new file mode 100644 index 0000000..8332d70 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.java @@ -0,0 +1,48 @@ +package com.repoachiever.service.integration.communication.registry; + +import com.repoachiever.exception.CommunicationConfigurationFailureException; +import com.repoachiever.service.config.ConfigService; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +/** + * Service used to perform initial communication infrastructure configuration. + */ +@Startup(value = 150) +@ApplicationScoped +public class RegistryCommunicationConfigService { + private static final Logger logger = LogManager.getLogger(RegistryCommunicationConfigService.class); + + @Inject + ConfigService configService; + + /** + * Performs initial communication infrastructure configuration. + */ + @PostConstruct + private void process() { + Registry registry; + + try { + registry = LocateRegistry.createRegistry( + configService.getConfig().getCommunication().getPort()); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + registry.list(); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.java new file mode 100644 index 0000000..ba9f04d --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.java @@ -0,0 +1,335 @@ +package com.repoachiever.service.integration.diagnostics; + +import com.repoachiever.dto.CommandExecutorOutputDto; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.*; +import com.repoachiever.service.command.docker.availability.DockerAvailabilityCheckCommandService; +import com.repoachiever.service.command.docker.inspect.remove.DockerInspectRemoveCommandService; +import com.repoachiever.service.command.docker.network.create.DockerNetworkCreateCommandService; +import com.repoachiever.service.command.docker.network.remove.DockerNetworkRemoveCommandService; +import com.repoachiever.service.command.grafana.GrafanaDeployCommandService; +import com.repoachiever.service.command.nodeexporter.NodeExporterDeployCommandService; +import com.repoachiever.service.command.prometheus.PrometheusDeployCommandService; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.executor.CommandExecutorService; +import com.repoachiever.service.telemetry.TelemetryService; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Objects; + +/** + * Service used to perform diagnostics infrastructure configuration operations. + */ +@Startup +@Priority(value = 190) +@ApplicationScoped +public class DiagnosticsConfigService { + private static final Logger logger = LogManager.getLogger(DiagnosticsConfigService.class); + + @Inject + PropertiesEntity properties; + + @Inject + ConfigService configService; + + @Inject + CommandExecutorService commandExecutorService; + + @Inject + DockerAvailabilityCheckCommandService dockerAvailabilityCheckCommandService; + + /** + * Creates Docker diagnostics network and deploys diagnostics infrastructure instances with pre-defined configurations. + */ + @PostConstruct + private void process() { + if (configService.getConfig().getDiagnostics().getEnabled()) { + CommandExecutorOutputDto dockerAvailabilityCommandOutput; + + try { + dockerAvailabilityCommandOutput = + commandExecutorService.executeCommand(dockerAvailabilityCheckCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerIsNotAvailableException(e.getMessage()).getMessage()); + return; + } + + String dockerAvailabilityCommandErrorOutput = dockerAvailabilityCommandOutput.getErrorOutput(); + + if ((Objects.nonNull(dockerAvailabilityCommandErrorOutput) && + !dockerAvailabilityCommandErrorOutput.isEmpty()) || + dockerAvailabilityCommandOutput.getNormalOutput().isEmpty()) { + logger.fatal(new DockerIsNotAvailableException(dockerAvailabilityCommandErrorOutput).getMessage()); + return; + } + + DockerInspectRemoveCommandService dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsPrometheusDockerName()); + + CommandExecutorOutputDto dockerInspectRemoveCommandOutput; + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + String dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + + + dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsPrometheusNodeExporterDockerName()); + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + + dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsGrafanaDockerName()); + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + + DockerNetworkCreateCommandService dockerNetworkCreateCommandService = + new DockerNetworkCreateCommandService(properties.getDiagnosticsCommonDockerNetworkName()); + + CommandExecutorOutputDto dockerNetworkCreateCommandOutput; + + try { + dockerNetworkCreateCommandOutput = + commandExecutorService.executeCommand(dockerNetworkCreateCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerNetworkCreateFailureException(e.getMessage()).getMessage()); + return; + } + + String dockerNetworkCreateCommandErrorOutput = dockerNetworkCreateCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerNetworkCreateCommandErrorOutput) && + !dockerNetworkCreateCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerNetworkCreateFailureException( + dockerNetworkCreateCommandErrorOutput).getMessage()); + } + + NodeExporterDeployCommandService nodeExporterDeployCommandService = + new NodeExporterDeployCommandService( + properties.getDiagnosticsPrometheusNodeExporterDockerName(), + properties.getDiagnosticsPrometheusNodeExporterDockerImage(), + configService.getConfig().getDiagnostics().getNodeExporter().getPort()); + + CommandExecutorOutputDto nodeExporterDeployCommandOutput; + + try { + nodeExporterDeployCommandOutput = + commandExecutorService.executeCommand(nodeExporterDeployCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new NodeExporterDeploymentFailureException(e.getMessage()).getMessage()); + return; + } + + String nodeExporterDeployCommandErrorOutput = nodeExporterDeployCommandOutput.getErrorOutput(); + + if (Objects.nonNull(nodeExporterDeployCommandErrorOutput) && + !nodeExporterDeployCommandErrorOutput.isEmpty()) { + logger.fatal(new NodeExporterDeploymentFailureException( + nodeExporterDeployCommandErrorOutput).getMessage()); + } + + PrometheusDeployCommandService prometheusDeployCommandService = + new PrometheusDeployCommandService( + properties.getDiagnosticsPrometheusDockerName(), + properties.getDiagnosticsPrometheusDockerImage(), + configService.getConfig().getDiagnostics().getPrometheus().getPort(), + properties.getDiagnosticsPrometheusConfigLocation(), + properties.getDiagnosticsPrometheusInternalLocation()); + + CommandExecutorOutputDto prometheusDeployCommandOutput; + + try { + prometheusDeployCommandOutput = + commandExecutorService.executeCommand(prometheusDeployCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new PrometheusDeploymentFailureException(e.getMessage()).getMessage()); + return; + } + + String prometheusDeployCommandErrorOutput = prometheusDeployCommandOutput.getErrorOutput(); + + if (Objects.nonNull(prometheusDeployCommandErrorOutput) && + !prometheusDeployCommandErrorOutput.isEmpty()) { + logger.fatal(new PrometheusDeploymentFailureException( + prometheusDeployCommandErrorOutput).getMessage()); + } + + GrafanaDeployCommandService grafanaDeployCommandService = + new GrafanaDeployCommandService( + properties.getDiagnosticsGrafanaDockerName(), + properties.getDiagnosticsGrafanaDockerImage(), + configService.getConfig().getDiagnostics().getGrafana().getPort(), + properties.getDiagnosticsGrafanaConfigLocation(), + properties.getDiagnosticsGrafanaInternalLocation()); + + CommandExecutorOutputDto grafanaDeployCommandOutput; + + try { + grafanaDeployCommandOutput = + commandExecutorService.executeCommand(grafanaDeployCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new PrometheusDeploymentFailureException(e.getMessage()).getMessage()); + return; + } + + String grafanaDeployCommandErrorOutput = grafanaDeployCommandOutput.getErrorOutput(); + + if (Objects.nonNull(grafanaDeployCommandErrorOutput) && + !grafanaDeployCommandErrorOutput.isEmpty()) { + logger.fatal(new PrometheusDeploymentFailureException( + grafanaDeployCommandErrorOutput).getMessage()); + } + } + } + + /** + * Removes created Docker networks and stops started diagnostics containers. + */ + @PreDestroy + private void close() { + if (configService.getConfig().getDiagnostics().getEnabled()) { + CommandExecutorOutputDto dockerAvailabilityCommandOutput; + + try { + dockerAvailabilityCommandOutput = + commandExecutorService.executeCommand(dockerAvailabilityCheckCommandService); + } catch (CommandExecutorException e) { + return; + } + + String dockerAvailabilityCommandErrorOutput = dockerAvailabilityCommandOutput.getErrorOutput(); + + if ((Objects.nonNull(dockerAvailabilityCommandErrorOutput) && + !dockerAvailabilityCommandErrorOutput.isEmpty()) || + dockerAvailabilityCommandOutput.getNormalOutput().isEmpty()) { + return; + } + + DockerNetworkRemoveCommandService dockerNetworkRemoveCommandService = + new DockerNetworkRemoveCommandService(properties.getDiagnosticsCommonDockerNetworkName()); + + CommandExecutorOutputDto dockerNetworkRemoveCommandOutput; + + try { + dockerNetworkRemoveCommandOutput = + commandExecutorService.executeCommand(dockerNetworkRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerNetworkRemoveFailureException(e.getMessage()).getMessage()); + return; + } + + String dockerNetworkRemoveCommandErrorOutput = dockerNetworkRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerNetworkRemoveCommandErrorOutput) && + !dockerNetworkRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerNetworkRemoveFailureException(dockerNetworkRemoveCommandErrorOutput).getMessage()); + } + + DockerInspectRemoveCommandService dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsPrometheusDockerName()); + + CommandExecutorOutputDto dockerInspectRemoveCommandOutput; + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + String dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + + dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsPrometheusNodeExporterDockerName()); + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + + dockerInspectRemoveCommandService = + new DockerInspectRemoveCommandService(properties.getDiagnosticsGrafanaDockerName()); + + try { + dockerInspectRemoveCommandOutput = + commandExecutorService.executeCommand(dockerInspectRemoveCommandService); + } catch (CommandExecutorException e) { + logger.fatal(new DockerInspectRemovalFailureException(e.getMessage()).getMessage()); + return; + } + + dockerInspectRemoveCommandErrorOutput = dockerInspectRemoveCommandOutput.getErrorOutput(); + + if (Objects.nonNull(dockerInspectRemoveCommandErrorOutput) && + !dockerInspectRemoveCommandErrorOutput.isEmpty()) { + logger.fatal(new DockerInspectRemovalFailureException( + dockerInspectRemoveCommandErrorOutput).getMessage()); + } + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.java new file mode 100644 index 0000000..a4b0a48 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.java @@ -0,0 +1,168 @@ +package com.repoachiever.service.integration.diagnostics.telemetry; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.TelemetryOperationFailureException; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.telemetry.binding.TelemetryBinding; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.binder.system.DiskSpaceMetrics; +import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Service used to perform diagnostics telemetry configuration operations. + */ +@Startup +@Priority(value = 200) +@ApplicationScoped +public class TelemetryConfigService { + private static final Logger logger = LogManager.getLogger(TelemetryConfigService.class); + + @Inject + PropertiesEntity properties; + + @Inject + ConfigService configService; + + @Inject + TelemetryBinding telemetryBinding; + + private ServerSocket connector; + + private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); + + /** + * Performs telemetry metrics server configuration, registering bindings provided by external provider. + */ + @PostConstruct + private void configure() { + try { + connector = new ServerSocket( + configService.getConfig().getDiagnostics().getMetrics().getPort()); + } catch (IOException e) { + logger.fatal(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + + new JvmThreadMetrics().bindTo(prometheusRegistry); + new JvmMemoryMetrics().bindTo(prometheusRegistry); + new DiskSpaceMetrics(new File(properties.getWorkspaceDirectory())).bindTo(prometheusRegistry); + new ProcessorMetrics().bindTo(prometheusRegistry); + new UptimeMetrics().bindTo(prometheusRegistry); + + telemetryBinding.bindTo(prometheusRegistry); + + Thread.ofPlatform().start(() -> { + while (!connector.isClosed()) { + Socket connection; + + try { + connection = connector.accept(); + } catch (IOException ignored) { + continue; + } + + try { + connection.setSoTimeout(properties.getDiagnosticsMetricsConnectionTimeout()); + } catch (SocketException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + executorService.execute(() -> { + OutputStreamWriter outputStreamWriter; + + try { + outputStreamWriter = + new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + BufferedReader inputStreamReader; + try { + inputStreamReader = new BufferedReader( + new InputStreamReader(connection.getInputStream())); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + outputStreamWriter.write( + String.format( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n%s", + prometheusRegistry.scrape())); + } catch (IOException ignored) { + return; + } + + try { + outputStreamWriter.flush(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + inputStreamReader.readLine(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + inputStreamReader.close(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + outputStreamWriter.close(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + return; + } + + try { + connection.close(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + } + }); + } + }); + } + + @PreDestroy + private void close() { + try { + connector.close(); + } catch (IOException e) { + logger.error(new TelemetryOperationFailureException(e.getMessage()).getMessage()); + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.java new file mode 100644 index 0000000..b3fd358 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.java @@ -0,0 +1,214 @@ +package com.repoachiever.service.integration.diagnostics.template; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.DiagnosticsTemplateProcessingFailureException; +import com.repoachiever.service.config.ConfigService; +import freemarker.cache.FileTemplateLoader; +import freemarker.template.*; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; + +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static freemarker.template.Configuration.VERSION_2_3_32; + +/** + * Service used to perform diagnostics template configuration operations. + */ +@Startup +@Priority(value = 180) +@ApplicationScoped +public class TemplateConfigService { + private static final Logger logger = LogManager.getLogger(TemplateConfigService.class); + + @Inject + PropertiesEntity properties; + + @Inject + ConfigService configService; + + /** + * Performs diagnostics infrastructure configuration templates parsing operations. + */ + @PostConstruct + private void process() { + if (configService.getConfig().getDiagnostics().getEnabled()) { + Configuration cfg = new Configuration(VERSION_2_3_32); + try { + cfg.setTemplateLoader(new FileTemplateLoader(new File(properties.getDiagnosticsPrometheusConfigLocation()))); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + cfg.setDefaultEncoding("UTF-8"); + + Template template; + + try { + template = cfg.getTemplate(properties.getDiagnosticsPrometheusConfigTemplate()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + Writer fileWriter; + + try { + fileWriter = new FileWriter( + Paths.get( + properties.getDiagnosticsPrometheusConfigLocation(), + properties.getDiagnosticsPrometheusConfigOutput()). + toFile()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + Map input = new HashMap<>() { + { + put("metrics", new HashMap() { + { + put("host", "host.docker.internal"); + put("port", String.valueOf(configService.getConfig().getDiagnostics().getMetrics().getPort())); + } + }); + put("nodeexporter", new HashMap() { + { + put("host", properties.getDiagnosticsPrometheusNodeExporterDockerName()); + put("port", String.valueOf(configService.getConfig().getDiagnostics().getNodeExporter().getPort())); + } + }); + } + }; + + try { + template.process(input, fileWriter); + } catch (TemplateException | IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } finally { + try { + fileWriter.close(); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } + } + + try { + cfg.setTemplateLoader(new FileTemplateLoader( + new File(properties.getDiagnosticsGrafanaDatasourcesLocation()))); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + try { + template = cfg.getTemplate(properties.getDiagnosticsGrafanaDatasourcesTemplate()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + try { + fileWriter = new FileWriter( + Paths.get( + properties.getDiagnosticsGrafanaDatasourcesLocation(), + properties.getDiagnosticsGrafanaDatasourcesOutput()). + toFile()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + input = new HashMap<>() { + { + put("prometheus", new HashMap() { + { + put("host", properties.getDiagnosticsPrometheusDockerName()); + put("port", String.valueOf(configService.getConfig().getDiagnostics().getPrometheus().getPort())); + } + }); + } + }; + + try { + template.process(input, fileWriter); + } catch (TemplateException | IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } finally { + try { + fileWriter.close(); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } + } + + try { + cfg.setTemplateLoader(new FileTemplateLoader( + new File(properties.getDiagnosticsGrafanaDashboardsLocation()))); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + try { + template = cfg.getTemplate(properties.getDiagnosticsGrafanaDashboardsDiagnosticsTemplate()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + try { + fileWriter = new FileWriter( + Paths.get( + properties.getDiagnosticsGrafanaDashboardsLocation(), + properties.getDiagnosticsGrafanaDashboardsDiagnosticsOutput()). + toFile()); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + return; + } + + input = new HashMap<>() { + { + put("info", new HashMap() { + { + put("version", properties.getGitCommitId()); + } + }); + put("nodeexporter", new HashMap() { + { + put("port", String.valueOf( + configService.getConfig().getDiagnostics().getNodeExporter().getPort())); + } + }); + } + }; + + try { + template.process(input, fileWriter); + } catch (TemplateException | IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } finally { + try { + fileWriter.close(); + } catch (IOException e) { + logger.fatal(new DiagnosticsTemplateProcessingFailureException(e.getMessage()).getMessage()); + } + } + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/http/HttpServerConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/http/HttpServerConfigService.java new file mode 100644 index 0000000..341f4f2 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/http/HttpServerConfigService.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.integration.http; + +import com.repoachiever.entity.common.ConfigEntity; +import com.repoachiever.service.config.ConfigService; +import io.quarkus.vertx.http.HttpServerOptionsCustomizer; +import io.vertx.core.http.HttpServerOptions; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Provides http server configuration used as a source of properties for all defined resources. + */ +@ApplicationScoped +public class HttpServerConfigService implements HttpServerOptionsCustomizer { + @Inject + ConfigService configService; + + /** + * @see HttpServerOptionsCustomizer + */ + @Override + public void customizeHttpServer(HttpServerOptions options) { + ConfigEntity.Connection connection = configService.getConfig().getConnection(); + + options.setPort(connection.getPort()); + } +} + diff --git a/api-server/src/main/java/com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.java new file mode 100644 index 0000000..782db0e --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.java @@ -0,0 +1,17 @@ +package com.repoachiever.service.integration.properties.general; + +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Service used to perform general properties configuration operations. + */ +@Startup +@ApplicationScoped +public class GeneralPropertiesConfigService { + @PostConstruct + private void process() { + + } +} \ No newline at end of file diff --git a/api-server/src/main/java/com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.java new file mode 100644 index 0000000..88653cc --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.java @@ -0,0 +1,75 @@ +package com.repoachiever.service.integration.properties.git; + +import io.quarkus.runtime.annotations.StaticInitSafe; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.microprofile.config.spi.ConfigSource; + +/** Provides access to external config source used as a source of Git info. */ +@StaticInitSafe +public class GitPropertiesConfigService implements ConfigSource { + private static final Logger logger = LogManager.getLogger(GitPropertiesConfigService.class); + + private static final String GIT_CONFIG_PROPERTIES_FILE = "git.properties"; + + private final Properties config; + + public GitPropertiesConfigService() { + this.config = new Properties(); + + ClassLoader classLoader = getClass().getClassLoader(); + InputStream gitBuildPropertiesStream = + classLoader.getResourceAsStream(GIT_CONFIG_PROPERTIES_FILE); + try { + config.load(gitBuildPropertiesStream); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } + + /** + * @see ConfigSource + */ + @Override + public Map getProperties() { + return ConfigSource.super.getProperties(); + } + + /** + * @see ConfigSource + */ + @Override + public Set getPropertyNames() { + return config.keySet().stream().map(element -> (String) element).collect(Collectors.toSet()); + } + + /** + * @see ConfigSource + */ + @Override + public int getOrdinal() { + return ConfigSource.super.getOrdinal(); + } + + /** + * @see ConfigSource + */ + @Override + public String getValue(String key) { + return (String) config.get(key); + } + + /** + * @see ConfigSource + */ + @Override + public String getName() { + return GitPropertiesConfigService.class.getSimpleName(); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/integration/state/StateConfigService.java b/api-server/src/main/java/com/repoachiever/service/integration/state/StateConfigService.java new file mode 100644 index 0000000..6a35e33 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/integration/state/StateConfigService.java @@ -0,0 +1,66 @@ +package com.repoachiever.service.integration.state; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.ApiServerInstanceIsAlreadyRunningException; +import com.repoachiever.service.state.StateService; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Service used to perform application critical state configuration. + */ +@Startup +@Priority(value = 140) +@ApplicationScoped +public class StateConfigService { + private static final Logger logger = LogManager.getLogger(StateConfigService.class); + + @Inject + PropertiesEntity properties; + + /** + * Performs application state initialization operations. + */ + @PostConstruct + private void process() { + Path running = Paths.get(properties.getStateLocation(), properties.getStateRunningName()); + + if (Files.exists(running)) { + logger.fatal(new ApiServerInstanceIsAlreadyRunningException().getMessage()); + return; + } + + try { + Files.createFile(running); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + + StateService.setStarted(true); + } + + /** + * Performs graceful application state cleanup after execution is finished. + */ + @PreDestroy + private void close() { + if (StateService.getStarted()) { + try { + Files.delete(Paths.get(properties.getStateLocation(), properties.getStateRunningName())); + } catch (IOException e) { + logger.error(e.getMessage()); + } + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/state/StateService.java b/api-server/src/main/java/com/repoachiever/service/state/StateService.java new file mode 100644 index 0000000..95d1579 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/state/StateService.java @@ -0,0 +1,80 @@ +package com.repoachiever.service.state; + +import com.repoachiever.dto.ClusterAllocationDto; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +/** + * Service used to operate as a collection of application state properties. + */ +public class StateService { + /** + * Represents if RepoAchiever API Server application has been started. + */ + @Getter + @Setter + private static Boolean started = false; + + /** + * Represents RepoAchiever Cluster topology state guard. + */ + @Getter + private final static ReentrantLock topologyStateGuard = new ReentrantLock(); + + + /** + * Represents a set of all available RepoAchiever Cluster allocations. + */ + @Getter + private final static List clusterAllocations = new ArrayList<>(); + + /** + * Retrieves RepoAchiever allocations with the given workspace unit key. + * + * @param workspaceUnitKey given workspace unit key. + * @return filtered RepoAchiever allocations according to the given workspace unit key. + */ + public static List getClusterAllocationsByWorkspaceUnitKey(String workspaceUnitKey) { + return clusterAllocations. + stream(). + filter(element -> Objects.equals(element.getWorkspaceUnitKey(), workspaceUnitKey)). + collect(Collectors.toList()); + } + + /** + * Adds new RepoAchiever Cluster allocations. + * + * @param allocations given RepoAchiever Cluster allocations. + */ + public static void addClusterAllocations(List allocations) { + clusterAllocations.addAll(allocations); + } + + /** + * Checks if RepoAchiever Cluster allocations with the given name exists. + * + * @param name given RepoAchiever Cluster allocation. + * @return result of the check. + */ + public static Boolean isClusterAllocationPresentByName(String name) { + return clusterAllocations + .stream() + .anyMatch(element -> Objects.equals(element.getName(), name)); + } + + /** + * Removes RepoAchiever Cluster allocations with the given names. + * + * @param names given RepoAchiever Cluster allocation names. + */ + public static void removeClusterAllocationByNames(List names) { + names.forEach( + element1 -> clusterAllocations.removeIf(element2 -> Objects.equals(element2.getName(), element1))); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/telemetry/TelemetryService.java b/api-server/src/main/java/com/repoachiever/service/telemetry/TelemetryService.java new file mode 100644 index 0000000..35706da --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/telemetry/TelemetryService.java @@ -0,0 +1,57 @@ +package com.repoachiever.service.telemetry; + +import com.repoachiever.service.telemetry.binding.TelemetryBinding; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Provides access to gather information and expose it to telemetry representation tool. + */ +@ApplicationScoped +public class TelemetryService { + @Inject + TelemetryBinding telemetryBinding; + + /** + * Increases allocated workers amount counter. + * + * @param value given increment value. + */ + public void increaseWorkersAmountBy(Integer value) { + telemetryBinding.getWorkerAmount().set(telemetryBinding.getWorkerAmount().get() + value); + } + + /** + * Decreases allocated workers amount counter. + * + * @param value given increment value. + */ + public void decreaseWorkersAmountBy(Integer value) { + telemetryBinding.getWorkerAmount().set(telemetryBinding.getWorkerAmount().get() - value); + } + + /** + * Increases allocated clusters amount counter. + * + * @param value given increment value. + */ + public void increaseClustersAmountBy(Integer value) { + telemetryBinding.getWorkerAmount().set(telemetryBinding.getWorkerAmount().get() + value); + } + + /** + * Decreases allocated clusters amount counter. + * + * @param value given increment value. + */ + public void decreaseClustersAmountBy(Integer value) { + telemetryBinding.getWorkerAmount().set(telemetryBinding.getWorkerAmount().get() - value); + } + + /** + * Records latest cluster allocation health check. + */ + public void recordClusterHealthCheck() { + + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/telemetry/binding/TelemetryBinding.java b/api-server/src/main/java/com/repoachiever/service/telemetry/binding/TelemetryBinding.java new file mode 100644 index 0000000..239e829 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/telemetry/binding/TelemetryBinding.java @@ -0,0 +1,44 @@ +package com.repoachiever.service.telemetry.binding; + +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import jakarta.enterprise.context.ApplicationScoped; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Service used to create custom telemetry bindings used to distribute application metrics. + */ +@ApplicationScoped +public class TelemetryBinding implements MeterBinder { + @Getter + private final AtomicInteger workerAmount = new AtomicInteger(); + + @Getter + private final AtomicInteger clusterAmount = new AtomicInteger(); + + @Getter + private Timer clusterHealthCheck; + + /** + * @see MeterBinder + */ + @Override + public void bindTo(@NotNull MeterRegistry meterRegistry) { + Gauge.builder("general.worker_amount", workerAmount, AtomicInteger::get) + .description("Represents amount of allocated workers") + .register(meterRegistry); + + Gauge.builder("general.cluster_amount", clusterAmount, AtomicInteger::get) + .description("Represents amount of allocated clusters") + .register(meterRegistry); + + clusterHealthCheck = Timer.builder("general.cluster_health_check") + .description("Represents all the performed health check requests for allocated clusters") + .register(meterRegistry); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/vendor/VendorFacade.java b/api-server/src/main/java/com/repoachiever/service/vendor/VendorFacade.java new file mode 100644 index 0000000..5c2c4b5 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/vendor/VendorFacade.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.vendor; + +import com.repoachiever.model.CredentialsFieldsExternal; +import com.repoachiever.model.Provider; +import com.repoachiever.service.vendor.git.github.GitGitHubVendorService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** Provides high-level access to VCS vendor operations. */ +@ApplicationScoped +public class VendorFacade { + @Inject + GitGitHubVendorService gitGitHubVendorService; + + /** + * Checks if the given external credentials are valid, according to the given provider name. + * + * @param provider given external provider name. + * @param credentialsFieldExternal given external credentials. + * @return result of the check. + */ + public Boolean isExternalCredentialsValid(Provider provider, CredentialsFieldsExternal credentialsFieldExternal) { + return switch (provider) { + case LOCAL -> true; + case GITHUB -> gitGitHubVendorService.isTokenValid(credentialsFieldExternal.getToken()); + }; + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/vendor/common/VendorConfigurationHelper.java b/api-server/src/main/java/com/repoachiever/service/vendor/common/VendorConfigurationHelper.java new file mode 100644 index 0000000..eab22c9 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/vendor/common/VendorConfigurationHelper.java @@ -0,0 +1,16 @@ +package com.repoachiever.service.vendor.common; + +/** + * Contains helpful tools used for vendor configuration. + */ +public class VendorConfigurationHelper { + /** + * Converts given raw token value to a wrapped format. + * + * @param token given raw token value. + * @return wrapped token. + */ + public static String getWrappedToken(String token) { + return String.format("Bearer %s", token); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java b/api-server/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java new file mode 100644 index 0000000..dbe5dd7 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java @@ -0,0 +1,33 @@ +package com.repoachiever.service.vendor.git.github; + +import com.repoachiever.service.client.github.IGitHubClientService; +import com.repoachiever.service.vendor.common.VendorConfigurationHelper; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import org.apache.http.HttpStatus; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@ApplicationScoped +public class GitGitHubVendorService { + @Inject + @RestClient + IGitHubClientService gitHubClientService; + + /** + * Checks if the given token is valid. + * + * @return result of the check. + */ + public boolean isTokenValid(String token) { + try { + Response response = gitHubClientService + .getOctocat(VendorConfigurationHelper.getWrappedToken(token)); + + return response.getStatus() == HttpStatus.SC_OK; + } catch (WebApplicationException e) { + return false; + } + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/workspace/WorkspaceService.java b/api-server/src/main/java/com/repoachiever/service/workspace/WorkspaceService.java new file mode 100644 index 0000000..f2aee94 --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/workspace/WorkspaceService.java @@ -0,0 +1,362 @@ +package com.repoachiever.service.workspace; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.repoachiever.entity.common.MetadataFileEntity; +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.exception.*; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.xml.bind.DatatypeConverter; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; + +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.util.FileSystemUtils; + +/** + * Represents local content workspace for different users. + */ +@ApplicationScoped +public class WorkspaceService { + private static final Logger logger = LogManager.getLogger(WorkspaceService.class); + + @Inject + PropertiesEntity properties; + + /** + * Creates unit key from the given segments. + * + * @param segments given segments to be used for unit key creation. + * @return created unit key from the given segments. + */ + @SneakyThrows + public String createUnitKey(String... segments) { + MessageDigest md = MessageDigest.getInstance("SHA3-256"); + return DatatypeConverter.printHexBinary(md.digest(String.join(".", segments).getBytes())); + } + + /** + * Creates content directory in the given workspace unit directory + * + * @param workspaceUnitDirectory given workspace unit directory + * @throws WorkspaceContentDirectoryCreationFailureException if workspace content directory creation operation failed. + */ + public void createContentDirectory(String workspaceUnitDirectory) throws + WorkspaceContentDirectoryCreationFailureException { + Path unitDirectoryPath = Path.of(workspaceUnitDirectory, properties.getWorkspaceContentDirectory()); + + if (Files.notExists(unitDirectoryPath)) { + try { + Files.createDirectory(unitDirectoryPath); + } catch (IOException e) { + throw new WorkspaceContentDirectoryCreationFailureException(e.getMessage()); + } + } + } + + /** + * Creates workspace unit with the help of the given key. + * + * @param key given workspace unit key. + * @throws WorkspaceUnitDirectoryCreationFailureException if workspace unit directory creation operation failed. + */ + public void createUnitDirectory(String key) throws + WorkspaceUnitDirectoryCreationFailureException { + Path unitDirectoryPath = Path.of(properties.getWorkspaceDirectory(), key); + + if (Files.notExists(unitDirectoryPath)) { + try { + Files.createDirectory(unitDirectoryPath); + } catch (IOException e) { + throw new WorkspaceUnitDirectoryCreationFailureException(e.getMessage()); + } + } + } + + /** + * Removes workspace unit with the help of the given key. + * + * @param key given workspace unit key. + * @throws WorkspaceUnitDirectoryRemovalFailureException if IO operation failed. + */ + public void removeUnitDirectory(String key) throws WorkspaceUnitDirectoryRemovalFailureException { + try { + FileSystemUtils.deleteRecursively(Path.of(properties.getWorkspaceDirectory(), key)); + } catch (IOException e) { + throw new WorkspaceUnitDirectoryRemovalFailureException(e.getMessage()); + } + } + + /** + * Checks if workspace unit directory with the help of the given key. + * + * @param key given workspace unit key. + * @return result if workspace unit directory exists with the help of the given key. + */ + public boolean isUnitDirectoryExist(String key) { + return Files.exists(Paths.get(properties.getWorkspaceDirectory(), key)); + } + + /** + * Retrieves path for the workspace unit with the help of the given key. + * + * @param key given workspace unit key. + * @throws WorkspaceUnitDirectoryNotFoundException if workspace unit with the given name does not + * exist. + */ + public String getUnitDirectory(String key) throws WorkspaceUnitDirectoryNotFoundException { + Path unitDirectoryPath = Path.of(properties.getWorkspaceDirectory(), key); + + if (Files.notExists(unitDirectoryPath)) { + throw new WorkspaceUnitDirectoryNotFoundException(); + } + + return unitDirectoryPath.toString(); + } + + /** + * Writes given content repository to the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param name given content repository name. + * @param input given content repository input. + * @throws ContentFileWriteFailureException if content file cannot be created. + */ + public void createContentFile(String workspaceUnitDirectory, String name, InputStream input) throws + ContentFileWriteFailureException { + Path contentDirectoryPath = Path.of(workspaceUnitDirectory, properties.getWorkspaceContentDirectory(), name); + + File file = new File(contentDirectoryPath.toString()); + + try { + FileUtils.copyInputStreamToFile(input, file); + } catch (IOException e) { + throw new ContentFileWriteFailureException(e.getMessage()); + } + } + + /** + * Removes content file in the given workspace unit. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param name given content repository name. + * @throws ContentFileRemovalFailureException if content file cannot be created. + */ + public void removeContentFile(String workspaceUnitDirectory, String name) throws + ContentFileRemovalFailureException { + try { + FileSystemUtils.deleteRecursively( + Path.of(workspaceUnitDirectory, properties.getWorkspaceContentDirectory(), name)); + } catch (IOException e) { + throw new ContentFileRemovalFailureException(e); + } + } + + /** + * Retrieves content file of the given name with the help of the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param name given name of the content file. + * @return content file entity. + * @throws ContentFileNotFoundException if the content file not found. + */ + public OutputStream getContentFile(String workspaceUnitDirectory, String name) throws + ContentFileNotFoundException { + Path contentDirectoryPath = Path.of(workspaceUnitDirectory, properties.getWorkspaceContentDirectory(), name); + + File file = new File(contentDirectoryPath.toString()); + + try { + return new FileOutputStream(file); + } catch (FileNotFoundException e) { + throw new ContentFileNotFoundException(e); + } + } + + /** + * Writes metadata file input of the given type to the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param type given type of the metadata file. + * @param input given metadata file entity input. + * @throws MetadataFileWriteFailureException if metadata file cannot be created. + */ + private void createMetadataFile(String workspaceUnitDirectory, String type, MetadataFileEntity input) + throws MetadataFileWriteFailureException { + ObjectMapper mapper = new ObjectMapper(); + + File variableFile = + new File( + Paths.get(workspaceUnitDirectory, properties.getWorkspaceMetadataDirectory(), type) + .toString()); + + try { + mapper.writeValue(variableFile, input); + } catch (IOException e) { + throw new MetadataFileWriteFailureException(e.getMessage()); + } + } + + /** + * Writes metadata file input of the prs type to the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param input given metadata file entity input. + * @throws MetadataFileWriteFailureException if metadata file cannot be created. + */ + public void createPRsMetadataFile(String workspaceUnitDirectory, MetadataFileEntity input) + throws MetadataFileWriteFailureException { + createMetadataFile(workspaceUnitDirectory, properties.getWorkspacePRsMetadataFileName(), input); + } + + /** + * Writes metadata file input of the issues type to the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param input given metadata file entity input. + * @throws MetadataFileWriteFailureException if metadata file cannot be created. + */ + public void createIssuesMetadataFile(String workspaceUnitDirectory, MetadataFileEntity input) + throws MetadataFileWriteFailureException { + createMetadataFile(workspaceUnitDirectory, properties.getWorkspaceIssuesMetadataFileName(), input); + } + + /** + * Writes metadata file input of the releases type to the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param input given metadata file entity input. + * @throws MetadataFileWriteFailureException if metadata file cannot be created. + */ + public void createReleasesMetadataFile(String workspaceUnitDirectory, MetadataFileEntity input) + throws MetadataFileWriteFailureException { + createMetadataFile(workspaceUnitDirectory, properties.getWorkspaceReleasesMetadataFileName(), input); + } + + /** + * Checks if metadata file of the given type exists in the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param type given type of the metadata file. + * @return result if metadata file exists in the given workspace unit directory. + */ + private boolean isMetadataFileExist(String workspaceUnitDirectory, String type) { + return Files.exists( + Paths.get(workspaceUnitDirectory, properties.getWorkspaceMetadataDirectory(), type)); + } + + /** + * Checks if metadata file of prs type exists in the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return result if metadata file exists in the given workspace unit directory. + */ + private boolean isPRsMetadataFileExist(String workspaceUnitDirectory) { + return isMetadataFileExist(workspaceUnitDirectory, properties.getWorkspacePRsMetadataFileName()); + } + + /** + * Checks if metadata file of issues type exists in the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return result if metadata file exists in the given workspace unit directory. + */ + private boolean isIssuesMetadataFileExist(String workspaceUnitDirectory) { + return isMetadataFileExist(workspaceUnitDirectory, properties.getWorkspaceIssuesMetadataFileName()); + } + + /** + * Checks if metadata file of releases type exists in the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return result if metadata file exists in the given workspace unit directory. + */ + private boolean isReleasesMetadataFileExist(String workspaceUnitDirectory) { + return isMetadataFileExist(workspaceUnitDirectory, properties.getWorkspaceReleasesMetadataFileName()); + } + + /** + * Retrieves metadata file content of the given type with the help of the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @param type given type of the metadata file. + * @return metadata file entity. + * @throws MetadataFileNotFoundException if the metadata file not found. + */ + public MetadataFileEntity getMetadataFileContent(String workspaceUnitDirectory, String type) + throws MetadataFileNotFoundException { + ObjectMapper mapper = + new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectReader reader = mapper.reader().forType(new TypeReference() { + }); + + InputStream variableFile; + try { + variableFile = + new FileInputStream( + Paths.get(workspaceUnitDirectory, properties.getWorkspaceMetadataDirectory(), type) + .toString()); + } catch (FileNotFoundException e) { + throw new MetadataFileNotFoundException(e.getMessage()); + } + + try { + return reader.readValues(variableFile).readAll().getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + + return null; + } + + /** + * Retrieves metadata file content of prs type with the help of the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return metadata file entity. + * @throws MetadataFileNotFoundException if the metadata variable file not found. + */ + public MetadataFileEntity getPRsMetadataFileContent(String workspaceUnitDirectory) + throws MetadataFileNotFoundException { + return getMetadataFileContent(workspaceUnitDirectory, properties.getWorkspacePRsMetadataFileName()); + } + + /** + * Retrieves metadata file content of issues type with the help of the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return metadata file entity. + * @throws MetadataFileNotFoundException if the metadata variable file not found. + */ + public MetadataFileEntity getIssuesMetadataFileContent(String workspaceUnitDirectory) + throws MetadataFileNotFoundException { + return getMetadataFileContent(workspaceUnitDirectory, properties.getWorkspaceIssuesMetadataFileName()); + } + + /** + * Retrieves metadata file content of releases type with the help of the given workspace unit directory. + * + * @param workspaceUnitDirectory given workspace unit directory. + * @return metadata file entity. + * @throws MetadataFileNotFoundException if the metadata variable file not found. + */ + public MetadataFileEntity getReleasesMetadataFileContent(String workspaceUnitDirectory) + throws MetadataFileNotFoundException { + return getMetadataFileContent(workspaceUnitDirectory, properties.getWorkspaceReleasesMetadataFileName()); + } +} diff --git a/api-server/src/main/java/com/repoachiever/service/workspace/facade/WorkspaceFacade.java b/api-server/src/main/java/com/repoachiever/service/workspace/facade/WorkspaceFacade.java new file mode 100644 index 0000000..45a17ff --- /dev/null +++ b/api-server/src/main/java/com/repoachiever/service/workspace/facade/WorkspaceFacade.java @@ -0,0 +1,114 @@ +package com.repoachiever.service.workspace.facade; + +import com.repoachiever.entity.common.PropertiesEntity; +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import com.repoachiever.service.workspace.WorkspaceService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.io.InputStream; + +/** + * Provides high-level access to workspace operations. + */ +@ApplicationScoped +public class WorkspaceFacade { + @Inject + PropertiesEntity properties; + + @Inject + WorkspaceService workspaceService; + + /** + * Creates unit key with the help of the given readiness check application. + * + * @param provider given provider. + * @param credentialsFields given credentials. + * @return result of the readiness check for the given configuration. + */ + public String createUnitKey(Provider provider, CredentialsFieldsFull credentialsFields) { + return switch (provider) { + case LOCAL -> workspaceService.createUnitKey(String.valueOf(credentialsFields.getInternal().getId())); + case GITHUB -> workspaceService.createUnitKey( + String.valueOf(credentialsFields.getInternal().getId()), credentialsFields.getExternal().getToken()); + }; + } + + /** + * + * @param input + * @param hash + * @param id + * @param provider + * @param credentialsFields + */ + public void addContent( + InputStream input, String hash, String id, Provider provider, CredentialsFieldsFull credentialsFields) { + + } + + /** + * + * @param id + * @param provider + * @param credentialsFields + */ + public void updatePRsMetadataFile( + String id, Provider provider, CredentialsFieldsFull credentialsFields) { + + } + + /** + * + * @param id + * @param provider + * @param credentialsFields + */ + public void updateIssuesMetadataFile( + String id, Provider provider, CredentialsFieldsFull credentialsFields) { + } + + /** + * + * @param id + * @param provider + * @param credentialsFields + */ + public void updateReleasesMetadataFile( + String id, Provider provider, CredentialsFieldsFull credentialsFields) { + } + +// /** +// * Update Kafka host in internal config file. +// * +// * @param provider given provider. +// * @param credentialsFields given credentials. +// * @throws WorkspaceUnitDirectoryNotFoundException if workspace unit directory was not found. +// * @throws InternalConfigNotFoundException if internal config file was not found. +// * @throws InternalConfigWriteFailureException if internal config file cannot be created. +// */ +// public void updateKafkaHost( +// String machineAdWorkspaceUnitDirectoryNotFoundExceptiondress, Provider provider, CredentialsFields credentialsFields) +// throws, +// InternalConfigNotFoundException, +// InternalConfigWriteFailureException { +// String workspaceUnitKey = createUnitKey(provider, credentialsFields); +// +// String workspaceUnitDirectory = workspaceService.getUnitDirectory(workspaceUnitKey); +// +// InternalConfigEntity internalConfig = null; +// +// if (workspaceService.isInternalConfigFileExist(workspaceUnitDirectory)) { +// internalConfig = workspaceService.getInternalConfigFileContent(workspaceUnitDirectory); +// } +// +// if (Objects.isNull(internalConfig)) { +// internalConfig = new InternalConfigEntity(); +// } +// +// internalConfig.getKafka().setHost(machineAddress); +// +// workspaceService.createInternalConfigFile(workspaceUnitDirectory, internalConfig); +// } +} diff --git a/api-server/src/main/openapi/openapi.yml b/api-server/src/main/openapi/openapi.yml new file mode 100644 index 0000000..240916a --- /dev/null +++ b/api-server/src/main/openapi/openapi.yml @@ -0,0 +1,352 @@ +openapi: 3.0.1 +info: + title: OpenAPI document of RepoAchiever API Server + description: RepoAchiever API Server Open API documentation + version: "1.0" + +tags: + - name: ContentResource + description: Contains all endpoints related to operations on processed content. + - name: StateResource + description: Contains all endpoints related to state processing. + - name: InfoResource + description: Contains all endpoints related to general info of API Server. + - name: HealthResource + description: Contains all endpoints related to general API Server health information. + +paths: + /v1/content: + post: + tags: + - ContentResource + requestBody: + required: true + description: Content retrieval application + content: + application/json: + schema: + $ref: "#/components/schemas/ContentRetrievalApplication" + responses: + 204: + description: A list of all available content + content: + application/json: + schema: + $ref: "#/components/schemas/ContentRetrievalResult" + /v1/content/apply: + post: + tags: + - ContentResource + requestBody: + required: true + description: Content configuration application + content: + application/json: + schema: + $ref: "#/components/schemas/ContentApplication" + responses: + 204: + description: Given content configuration was successfully applied + 400: + description: Given content configuration was not applied + /v1/content/withdraw: + delete: + tags: + - ContentResource + requestBody: + required: true + description: Content withdraw application. Does not remove persisted content. + content: + application/json: + schema: + $ref: "#/components/schemas/ContentWithdrawal" + responses: + 204: + description: Given content configuration was successfully withdrawn + 400: + description: Given content configuration was not withdrawn + /v1/content/download: + get: + tags: + - ContentResource + parameters: + - in: query + name: location + schema: + type: string + description: Name of content location to be downloaded + responses: + 200: + description: A content was successfully retrieved + content: + application/octet-stream: + schema: + type: string + format: binary + /v1/content/clean: + post: + tags: + - ContentResource + requestBody: + required: true + description: Content configuration application + content: + application/json: + schema: + $ref: "#/components/schemas/ContentCleanup" + responses: + 201: + description: Content with the given configuration was successfully deleted + 400: + description: Content with the given configuration was not deleted + /v1/state/content: + post: + tags: + - StateResource + requestBody: + required: true + description: Given content state key + content: + application/json: + schema: + $ref: "#/components/schemas/ContentStateApplication" + responses: + 201: + description: Content state hash is retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/ContentStateApplicationResult" + /v1/info/version: + get: + tags: + - InfoResource + responses: + 200: + description: General information about running API Server + content: + application/json: + schema: + $ref: "#/components/schemas/VersionInfoResult" + /v1/info/cluster: + get: + tags: + - InfoResource + responses: + 200: + description: General information about running clusters + content: + application/json: + schema: + $ref: "#/components/schemas/ClusterInfoResult" + /v1/info/telemetry: + get: + tags: + - InfoResource + responses: + 200: + description: A set of Prometheus samples used by Grafana instance + content: + text/plain: + schema: + type: string + /v1/health: + get: + tags: + - HealthResource + responses: + 200: + description: General health information about running API Server + content: + application/json: + schema: + $ref: "#/components/schemas/HealthCheckResult" + /v1/readiness: + post: + tags: + - HealthResource + requestBody: + required: true + description: Check if API Server is ready to serve for the given user + content: + application/json: + schema: + $ref: "#/components/schemas/ReadinessCheckApplication" + responses: + 200: + description: General health information about running API Server + content: + application/json: + schema: + $ref: "#/components/schemas/ReadinessCheckResult" +components: + schemas: + Provider: + type: string + enum: + - git-local + - git-github + CredentialsFieldsFull: + required: + - internal + properties: + internal: + $ref: "#/components/schemas/CredentialsFieldsInternal" + external: + $ref: "#/components/schemas/CredentialsFieldsExternal" + CredentialsFieldsInternal: + properties: + id: + type: integer + CredentialsFieldsExternal: + anyOf: + - $ref: "#/components/schemas/GitGitHubCredentials" + GitGitHubCredentials: + required: + - token + properties: + token: + type: string + ContentRetrievalApplication: + required: + - provider + - credentials + properties: + provider: + $ref: "#/components/schemas/Provider" + credentials: + $ref: "#/components/schemas/CredentialsFieldsFull" + ContentRetrievalResult: + required: + - locations + properties: + locations: + type: array + items: + type: string + ContentApplication: + required: + - locations + - provider + - credentials + properties: + locations: + type: array + items: + type: string + provider: + $ref: "#/components/schemas/Provider" + credentials: + $ref: "#/components/schemas/CredentialsFieldsFull" + ContentWithdrawal: + required: + - credentials + - provider + properties: + provider: + $ref: "#/components/schemas/Provider" + credentials: + $ref: "#/components/schemas/CredentialsFieldsFull" + ContentCleanup: + required: + - credentials + properties: + credentials: + $ref: "#/components/schemas/CredentialsFieldsFull" + ContentStateApplication: + required: + - provider + - credentials + properties: + provider: + $ref: "#/components/schemas/Provider" + credentials: + $ref: "#/components/schemas/CredentialsFieldsFull" + ContentStateApplicationResult: + required: + - hash + properties: + hash: + type: string + VersionInfoResult: + properties: + externalApi: + $ref: "#/components/schemas/VersionExternalApiInfoResult" + VersionExternalApiInfoResult: + required: + - version + - hash + properties: + version: + type: string + hash: + type: string + ClusterInfoResult: + type: array + items: + $ref: "#/components/schemas/ClusterInfoUnit" + ClusterInfoUnit: + required: + - name + properties: + name: + type: string + health: + type: boolean + workers: + type: integer + HealthCheckResult: + required: + - status + - checks + properties: + status: + $ref: "#/components/schemas/HealthCheckStatus" + checks: + type: array + items: + $ref: "#/components/schemas/HealthCheckUnit" + HealthCheckUnit: + required: + - name + - status + properties: + name: + type: string + status: + $ref: "#/components/schemas/HealthCheckStatus" + HealthCheckStatus: + type: string + enum: + - UP + - DOWN + ReadinessCheckApplication: + properties: + test: + type: object + ReadinessCheckResult: + required: + - name + - status + - data + properties: + name: + type: string + status: + $ref: "#/components/schemas/ReadinessCheckStatus" + data: + type: object + ReadinessCheckUnit: + required: + - name + - status + properties: + name: + type: string + status: + $ref: "#/components/schemas/ReadinessCheckStatus" + ReadinessCheckStatus: + type: string + enum: + - UP + - DOWN diff --git a/api-server/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/api-server/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource new file mode 100644 index 0000000..1803d67 --- /dev/null +++ b/api-server/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource @@ -0,0 +1 @@ +com.repoachiever.service.integration.properties.git.GitPropertiesConfigService \ No newline at end of file diff --git a/api-server/src/main/resources/application.properties b/api-server/src/main/resources/application.properties new file mode 100644 index 0000000..f8772cd --- /dev/null +++ b/api-server/src/main/resources/application.properties @@ -0,0 +1,155 @@ +# Describes general internal Quarkus configuration. +quarkus.http.cors=false +quarkus.smallrye-health.ui.always-include=true +quarkus.swagger-ui.always-include=true +quarkus.native.builder-image=graalvm + +# Describes database Quarkus configuration. +quarkus.datasource.jdbc.driver=org.sqlite.JDBC +quarkus.datasource.db-kind=other +quarkus.datasource.jdbc.url=jdbc:sqlite:${user.home}/.repoachiever/internal/database/data.db +quarkus.datasource.username=repoachiever_user +quarkus.datasource.password=repoachiever_password + +# Describes LiquiBase Quarkus configuration. +quarkus.liquibase.change-log=liquibase/config.yaml +quarkus.liquibase.migrate-at-start=true + +# Describes external Quarkus clients configuration. +quarkus.rest-client.github.url=https://api.github.com +quarkus.rest-client.small-rye-health-check.url=http://${quarkus.http.host}:${quarkus.http.port} + +# Describes location of RepoAchiever API Server states. +state.location=${user.home}/.repoachiever/internal/state + +# Describes name of RepoAchiever API Server running state. +state.running.name=.running + +# Describes database config table name. +database.tables.config.name=config + +# Describes database content table name. +database.tables.content.name=content + +# Describes database provider table name. +database.tables.provider.name=provider + +# Describes database secrets table name. +database.tables.secret.name=secret + +# Describes database statement close delay duration. +database.statement.close-delay=10000 + +# Describes git configuration properties file. +git.config.location=git.properties + +# Describes location of RepoAchiever executable files. +bin.directory=${user.home}/.repoachiever/bin + +# Describes location of RepoAchiever Cluster executable file. +bin.cluster.location=cluster/cluster.jar + +# Describes location of application configurations. +config.directory=${user.home}/.repoachiever/config + +# Describes name of the application configuration file. +config.name=api-server.yaml + +# Describes location of local workspace. +workspace.directory=${user.home}/.repoachiever/workspace + +# Describes location of content directory. +workspace.content.directory=content + +# Describes max amount of versions for each content repository. +workspace.content.version-amount=5 + +# Describes location of metadata directory. +workspace.metadata.directory=metadata + +# Describes location of pull requests metadata file. +workspace.prs-metadata-file.name=prs.json + +# Describes location of issue metadata file. +workspace.issues-metadata-file.name=issues.json + +# Describes location of releases metadata file. +workspace.releases-metadata-file.name=releases.json + +# Describes RepoAchiever Cluster context environment variable name. +repoachiever-cluster.context.alias=REPOACHIEVER_CLUSTER_CONTEXT + +# Describes RepoAchiever API Server communication provider name. +communication.api-server.name=repoachiever-api-server + +# Describes RepoAchiever Cluster allocation base prefix. +communication.cluster.base=repoachiever-cluster + +# Describes RepoAchiever Cluster startup await frequency duration. +communication.cluster.startup-await-frequency=1000 + +# Describes RepoAchiever Cluster startup timeout duration. +communication.cluster.startup-timeout=10000 + +# Describes RepoAchiever Cluster health check operation frequency duration. +communication.cluster.health-check-frequency=1000 + +# Describes name of the Docker network used to install diagnostics infrastructure. +diagnostics.common.docker.network.name=repoachiever-cluster + +# Describes location of Grafana configuration files. +diagnostics.grafana.config.location=${user.home}/.repoachiever/diagnostics/grafana/config + +# Describes location of Grafana datasources configuration files. +diagnostics.grafana.datasources.location=${user.home}/.repoachiever/diagnostics/grafana/config/datasources + +# Describes name of Grafana configuration template file. +diagnostics.grafana.datasources.template=datasource.tmpl + +# Describes name of Grafana configuration template processing output file. +diagnostics.grafana.datasources.output=datasource.yml + +# Describes location of Grafana dashboards configuration files. +diagnostics.grafana.dashboards.location=${user.home}/.repoachiever/diagnostics/grafana/config/dashboards + +# Describes location of Grafana diagnostics dashboards configuration files. +diagnostics.grafana.dashboards.diagnostics.template=diagnostics.tmpl + +# Describes location of Grafana diagnostics dashboards configuration files. +diagnostics.grafana.dashboards.diagnostics.output=diagnostics.json + +# Describes location of Grafana internal files. +diagnostics.grafana.internal.location=${user.home}/.repoachiever/diagnostics/grafana/internal + +# Describes name of the Docker container used for Grafana instance deployment. +diagnostics.grafana.docker.name=repoachiever-diagnostics-grafana + +# Describes image name of the Docker container used for Grafana instance deployment. +diagnostics.grafana.docker.image=grafana/grafana + +# Describes location of Prometheus configuration files. +diagnostics.prometheus.config.location=${user.home}/.repoachiever/diagnostics/prometheus/config + +# Describes name of Prometheus configuration template file. +diagnostics.prometheus.config.template=prometheus.tmpl + +# Describes name of Prometheus configuration template processing output file. +diagnostics.prometheus.config.output=prometheus.yml + +# Describes location of Prometheus internal files. +diagnostics.prometheus.internal.location=${user.home}/.repoachiever/diagnostics/prometheus/internal + +# Describes name of the Docker container used for Prometheus instance deployment. +diagnostics.prometheus.docker.name=repoachiever-diagnostics-prometheus + +# Describes image name of the Docker container used for Prometheus instance deployment. +diagnostics.prometheus.docker.image=prom/prometheus:v2.36.2 + +# Describes name of the Docker container used for Prometheus Node Exporter instance deployment. +diagnostics.prometheus.node-exporter.docker.name=repoachiever-diagnostics-prometheus-node-exporter + +# Describes image name of the Docker container used for Prometheus Node Exporter instance deployment. +diagnostics.prometheus.node-exporter.docker.image=quay.io/prometheus/node-exporter:latest + +# Describes connection timeout used by metrics service. +diagnostics.metrics.connection.timeout=3000 \ No newline at end of file diff --git a/api-server/src/main/resources/liquibase/config.yaml b/api-server/src/main/resources/liquibase/config.yaml new file mode 100644 index 0000000..ef09d45 --- /dev/null +++ b/api-server/src/main/resources/liquibase/config.yaml @@ -0,0 +1,104 @@ +databaseChangeLog: + - changeSet: + id: 1 + author: YarikRevich + changes: + - createTable: + tableName: config + columns: + - column: + name: id + type: INT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: VARCHAR + constraints: + unique: true + nullable: false + - column: + name: hash + type: VARCHAR + constraints: + nullable: false + - createTable: + tableName: secret + columns: + - column: + name: id + type: INT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: session + type: INT + constraints: + nullable: false + - column: + name: credentials + type: VARCHAR + constraints: + nullable: true + - createTable: + tableName: provider + columns: + - column: + name: id + type: INT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: VARCHAR + constraints: + unique: true + nullable: false + - createTable: + tableName: content + columns: + - column: + name: id + type: INT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: location + type: VARCHAR + constraints: + nullable: false + - column: + name: provider + type: INT + constraints: + foreignKeyName: provider_fk + references: provider(id) + nullable: false + - column: + name: secret + type: INT + constraints: + foreignKeyName: secret_fk + references: secret(id) + nullable: false + - loadData: + tableName: provider + usePreparedStatements: false + separator: ; + relativeToChangelogFile: true + file: data/data.csv + encoding: UTF-8 + quotchar: '''' + columns: + - column: + header: Name + name: name + type: STRING \ No newline at end of file diff --git a/api-server/src/main/resources/liquibase/data/data.csv b/api-server/src/main/resources/liquibase/data/data.csv new file mode 100644 index 0000000..5b0462b --- /dev/null +++ b/api-server/src/main/resources/liquibase/data/data.csv @@ -0,0 +1,3 @@ +Name +git-local +git-github \ No newline at end of file diff --git a/api-server/src/main/resources/log4j2.xml b/api-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..e47463a --- /dev/null +++ b/api-server/src/main/resources/log4j2.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + + + + + + + + diff --git a/api-server/target/classes/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat b/api-server/target/classes/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat new file mode 100644 index 0000000000000000000000000000000000000000..1a80445a73cff114c002acdbc47e2a96da7b99e3 GIT binary patch literal 81 zcmZQzU|?imNzN}yWdN~w(-KP(a}o;*Qu9($ix||B^K + + + + + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + + + + + + + + diff --git a/api-server/target/failsafe-reports/failsafe-summary.xml b/api-server/target/failsafe-reports/failsafe-summary.xml new file mode 100644 index 0000000..fffbf43 --- /dev/null +++ b/api-server/target/failsafe-reports/failsafe-summary.xml @@ -0,0 +1,8 @@ + + + 0 + 0 + 0 + 0 + + \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/.dockerignore b/api-server/target/generated-sources/openapi/.dockerignore new file mode 100644 index 0000000..b86c7ac --- /dev/null +++ b/api-server/target/generated-sources/openapi/.dockerignore @@ -0,0 +1,4 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/.openapi-generator-ignore b/api-server/target/generated-sources/openapi/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/api-server/target/generated-sources/openapi/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/api-server/target/generated-sources/openapi/.openapi-generator/FILES b/api-server/target/generated-sources/openapi/.openapi-generator/FILES new file mode 100644 index 0000000..ced31dc --- /dev/null +++ b/api-server/target/generated-sources/openapi/.openapi-generator/FILES @@ -0,0 +1,35 @@ +.dockerignore +.openapi-generator-ignore +README.md +pom.xml +src/main/docker/Dockerfile.jvm +src/main/docker/Dockerfile.native +src/main/java/com/repoachiever/RestResourceRoot.java +src/main/java/com/repoachiever/api/ContentResourceApi.java +src/main/java/com/repoachiever/api/HealthResourceApi.java +src/main/java/com/repoachiever/api/InfoResourceApi.java +src/main/java/com/repoachiever/api/StateResourceApi.java +src/main/java/com/repoachiever/model/ClusterInfoUnit.java +src/main/java/com/repoachiever/model/ContentApplication.java +src/main/java/com/repoachiever/model/ContentCleanup.java +src/main/java/com/repoachiever/model/ContentRetrievalApplication.java +src/main/java/com/repoachiever/model/ContentRetrievalResult.java +src/main/java/com/repoachiever/model/ContentStateApplication.java +src/main/java/com/repoachiever/model/ContentStateApplicationResult.java +src/main/java/com/repoachiever/model/ContentWithdrawal.java +src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java +src/main/java/com/repoachiever/model/CredentialsFieldsFull.java +src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java +src/main/java/com/repoachiever/model/GitGitHubCredentials.java +src/main/java/com/repoachiever/model/HealthCheckResult.java +src/main/java/com/repoachiever/model/HealthCheckStatus.java +src/main/java/com/repoachiever/model/HealthCheckUnit.java +src/main/java/com/repoachiever/model/Provider.java +src/main/java/com/repoachiever/model/ReadinessCheckApplication.java +src/main/java/com/repoachiever/model/ReadinessCheckResult.java +src/main/java/com/repoachiever/model/ReadinessCheckStatus.java +src/main/java/com/repoachiever/model/ReadinessCheckUnit.java +src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java +src/main/java/com/repoachiever/model/VersionInfoResult.java +src/main/resources/META-INF/openapi.yaml +src/main/resources/application.properties diff --git a/api-server/target/generated-sources/openapi/.openapi-generator/VERSION b/api-server/target/generated-sources/openapi/.openapi-generator/VERSION new file mode 100644 index 0000000..c0be8a7 --- /dev/null +++ b/api-server/target/generated-sources/openapi/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.4.0 \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/.openapi-generator/openapi.yml-default.sha256 b/api-server/target/generated-sources/openapi/.openapi-generator/openapi.yml-default.sha256 new file mode 100644 index 0000000..229ea46 --- /dev/null +++ b/api-server/target/generated-sources/openapi/.openapi-generator/openapi.yml-default.sha256 @@ -0,0 +1 @@ +23dd5ef7368f378589d86135d0c9e3602c82eeb0dd992c1c2efe429c7811b94e \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/README.md b/api-server/target/generated-sources/openapi/README.md new file mode 100644 index 0000000..578b4eb --- /dev/null +++ b/api-server/target/generated-sources/openapi/README.md @@ -0,0 +1,15 @@ +# JAX-RS server with OpenAPI using Quarkus + +## Overview +This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using an +[OpenAPI-Spec](https://openapis.org), you can easily generate a server stub. + +This is an example of building a OpenAPI-enabled JAX-RS server. +This example uses the [JAX-RS](https://jax-rs-spec.java.net/) framework and +the [Eclipse-MicroProfile-OpenAPI](https://github.com/eclipse/microprofile-open-api) addition. + +The pom file is configured to use [Quarkus](https://quarkus.io/) as application server. + +This project produces a jar that defines some interfaces. +The jar can be used in combination with another project providing the implementation. + diff --git a/api-server/target/generated-sources/openapi/pom.xml b/api-server/target/generated-sources/openapi/pom.xml new file mode 100644 index 0000000..b1e2d58 --- /dev/null +++ b/api-server/target/generated-sources/openapi/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + org.openapitools + openapi-jaxrs-client + openapi-jaxrs-client + 1.0 + + + + 3.8.1 + true + 1.8 + 1.8 + UTF-8 + UTF-8 + 1.1.1.Final + quarkus-universe-bom + io.quarkus + 1.1.1.Final + 2.22.1 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-smallrye-openapi + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-source + generate-sources + + add-source + + + + src/gen/java + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus-plugin.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + native + + + + diff --git a/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.jvm b/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..87730c9 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.jvm @@ -0,0 +1,34 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the docker image run: +# +# mvn package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/openapi-jaxrs-client-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/openapi-jaxrs-client-jvm +# +### +FROM fabric8/java-alpine-openjdk8-jre:1.6.5 +ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV AB_ENABLED=jmx_exporter + +# Be prepared for running in OpenShift too +RUN adduser -G root --no-create-home --disabled-password 1001 \ + && chown -R 1001 /deployments \ + && chmod -R "g+rwX" /deployments \ + && chown -R 1001:root /deployments + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/app.jar +EXPOSE 8080 + +# run with user 1001 +USER 1001 + +ENTRYPOINT [ "/deployments/run-java.sh" ] \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.native b/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..1a46ca0 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/docker/Dockerfile.native @@ -0,0 +1,22 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode +# +# Before building the docker image run: +# +# mvn package -Pnative -Dquarkus.native.container-build=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/openapi-jaxrs-client . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/openapi-jaxrs-client +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal +WORKDIR /work/ +COPY target/*-runner /work/application +RUN chmod 775 /work +EXPOSE 8080 +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] \ No newline at end of file diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/RestResourceRoot.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/RestResourceRoot.java new file mode 100644 index 0000000..d213a41 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/RestResourceRoot.java @@ -0,0 +1,5 @@ +package com.repoachiever; + +public class RestResourceRoot { + public static final String APPLICATION_PATH = ""; +} diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/ContentResourceApi.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/ContentResourceApi.java new file mode 100644 index 0000000..d67cf06 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/ContentResourceApi.java @@ -0,0 +1,49 @@ +package com.repoachiever.api; + +import com.repoachiever.model.ContentApplication; +import com.repoachiever.model.ContentCleanup; +import com.repoachiever.model.ContentRetrievalApplication; +import com.repoachiever.model.ContentRetrievalResult; +import com.repoachiever.model.ContentWithdrawal; +import java.io.File; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + + +import java.io.InputStream; +import java.util.Map; +import java.util.List; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +@Path("/v1/content") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]") +public interface ContentResourceApi { + + @POST + @Path("/apply") + @Consumes({ "application/json" }) + void v1ContentApplyPost(@Valid @NotNull ContentApplication contentApplication); + + @POST + @Path("/clean") + @Consumes({ "application/json" }) + void v1ContentCleanPost(@Valid @NotNull ContentCleanup contentCleanup); + + @GET + @Path("/download") + @Produces({ "application/octet-stream" }) + File v1ContentDownloadGet(@QueryParam("location") String location); + + @POST + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + ContentRetrievalResult v1ContentPost(@Valid @NotNull ContentRetrievalApplication contentRetrievalApplication); + + @DELETE + @Path("/withdraw") + @Consumes({ "application/json" }) + void v1ContentWithdrawDelete(@Valid @NotNull ContentWithdrawal contentWithdrawal); +} diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/HealthResourceApi.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/HealthResourceApi.java new file mode 100644 index 0000000..803db5f --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/HealthResourceApi.java @@ -0,0 +1,32 @@ +package com.repoachiever.api; + +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.model.ReadinessCheckApplication; +import com.repoachiever.model.ReadinessCheckResult; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + + +import java.io.InputStream; +import java.util.Map; +import java.util.List; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +@Path("/v1") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]") +public interface HealthResourceApi { + + @GET + @Path("/health") + @Produces({ "application/json" }) + HealthCheckResult v1HealthGet(); + + @POST + @Path("/readiness") + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + ReadinessCheckResult v1ReadinessPost(@Valid @NotNull ReadinessCheckApplication readinessCheckApplication); +} diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/InfoResourceApi.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/InfoResourceApi.java new file mode 100644 index 0000000..14be2b6 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/InfoResourceApi.java @@ -0,0 +1,35 @@ +package com.repoachiever.api; + +import com.repoachiever.model.ClusterInfoUnit; +import com.repoachiever.model.VersionInfoResult; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + + +import java.io.InputStream; +import java.util.Map; +import java.util.List; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +@Path("/v1/info") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]") +public interface InfoResourceApi { + + @GET + @Path("/cluster") + @Produces({ "application/json" }) + List v1InfoClusterGet(); + + @GET + @Path("/telemetry") + @Produces({ "text/plain" }) + String v1InfoTelemetryGet(); + + @GET + @Path("/version") + @Produces({ "application/json" }) + VersionInfoResult v1InfoVersionGet(); +} diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/StateResourceApi.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/StateResourceApi.java new file mode 100644 index 0000000..ed43a92 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/StateResourceApi.java @@ -0,0 +1,25 @@ +package com.repoachiever.api; + +import com.repoachiever.model.ContentStateApplication; +import com.repoachiever.model.ContentStateApplicationResult; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + + + +import java.io.InputStream; +import java.util.Map; +import java.util.List; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +@Path("/v1/state/content") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]") +public interface StateResourceApi { + + @POST + @Consumes({ "application/json" }) + @Produces({ "application/json" }) + ContentStateApplicationResult v1StateContentPost(@Valid @NotNull ContentStateApplication contentStateApplication); +} diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ClusterInfoUnit.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ClusterInfoUnit.java new file mode 100644 index 0000000..da62edc --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ClusterInfoUnit.java @@ -0,0 +1,123 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ClusterInfoUnit") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ClusterInfoUnit implements Serializable { + private @Valid String name; + private @Valid Boolean health; + private @Valid Integer workers; + + /** + **/ + public ClusterInfoUnit name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public ClusterInfoUnit health(Boolean health) { + this.health = health; + return this; + } + + + @JsonProperty("health") + public Boolean getHealth() { + return health; + } + + @JsonProperty("health") + public void setHealth(Boolean health) { + this.health = health; + } + + /** + **/ + public ClusterInfoUnit workers(Integer workers) { + this.workers = workers; + return this; + } + + + @JsonProperty("workers") + public Integer getWorkers() { + return workers; + } + + @JsonProperty("workers") + public void setWorkers(Integer workers) { + this.workers = workers; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClusterInfoUnit clusterInfoUnit = (ClusterInfoUnit) o; + return Objects.equals(this.name, clusterInfoUnit.name) && + Objects.equals(this.health, clusterInfoUnit.health) && + Objects.equals(this.workers, clusterInfoUnit.workers); + } + + @Override + public int hashCode() { + return Objects.hash(name, health, workers); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ClusterInfoUnit {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" health: ").append(toIndentedString(health)).append("\n"); + sb.append(" workers: ").append(toIndentedString(workers)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentApplication.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentApplication.java new file mode 100644 index 0000000..dc4b33e --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentApplication.java @@ -0,0 +1,145 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentApplication") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentApplication implements Serializable { + private @Valid List locations = new ArrayList<>(); + private @Valid Provider provider; + private @Valid CredentialsFieldsFull credentials; + + /** + **/ + public ContentApplication locations(List locations) { + this.locations = locations; + return this; + } + + + @JsonProperty("locations") + @NotNull + public List getLocations() { + return locations; + } + + @JsonProperty("locations") + public void setLocations(List locations) { + this.locations = locations; + } + + public ContentApplication addLocationsItem(String locationsItem) { + if (this.locations == null) { + this.locations = new ArrayList<>(); + } + + this.locations.add(locationsItem); + return this; + } + + public ContentApplication removeLocationsItem(String locationsItem) { + if (locationsItem != null && this.locations != null) { + this.locations.remove(locationsItem); + } + + return this; + } + /** + **/ + public ContentApplication provider(Provider provider) { + this.provider = provider; + return this; + } + + + @JsonProperty("provider") + @NotNull + public Provider getProvider() { + return provider; + } + + @JsonProperty("provider") + public void setProvider(Provider provider) { + this.provider = provider; + } + + /** + **/ + public ContentApplication credentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + return this; + } + + + @JsonProperty("credentials") + @NotNull + public CredentialsFieldsFull getCredentials() { + return credentials; + } + + @JsonProperty("credentials") + public void setCredentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentApplication contentApplication = (ContentApplication) o; + return Objects.equals(this.locations, contentApplication.locations) && + Objects.equals(this.provider, contentApplication.provider) && + Objects.equals(this.credentials, contentApplication.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(locations, provider, credentials); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentApplication {\n"); + + sb.append(" locations: ").append(toIndentedString(locations)).append("\n"); + sb.append(" provider: ").append(toIndentedString(provider)).append("\n"); + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentCleanup.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentCleanup.java new file mode 100644 index 0000000..2970568 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentCleanup.java @@ -0,0 +1,82 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsFull; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentCleanup") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentCleanup implements Serializable { + private @Valid CredentialsFieldsFull credentials; + + /** + **/ + public ContentCleanup credentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + return this; + } + + + @JsonProperty("credentials") + @NotNull + public CredentialsFieldsFull getCredentials() { + return credentials; + } + + @JsonProperty("credentials") + public void setCredentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentCleanup contentCleanup = (ContentCleanup) o; + return Objects.equals(this.credentials, contentCleanup.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(credentials); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentCleanup {\n"); + + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalApplication.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalApplication.java new file mode 100644 index 0000000..1c1dcfa --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalApplication.java @@ -0,0 +1,105 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentRetrievalApplication") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentRetrievalApplication implements Serializable { + private @Valid Provider provider; + private @Valid CredentialsFieldsFull credentials; + + /** + **/ + public ContentRetrievalApplication provider(Provider provider) { + this.provider = provider; + return this; + } + + + @JsonProperty("provider") + @NotNull + public Provider getProvider() { + return provider; + } + + @JsonProperty("provider") + public void setProvider(Provider provider) { + this.provider = provider; + } + + /** + **/ + public ContentRetrievalApplication credentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + return this; + } + + + @JsonProperty("credentials") + @NotNull + public CredentialsFieldsFull getCredentials() { + return credentials; + } + + @JsonProperty("credentials") + public void setCredentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentRetrievalApplication contentRetrievalApplication = (ContentRetrievalApplication) o; + return Objects.equals(this.provider, contentRetrievalApplication.provider) && + Objects.equals(this.credentials, contentRetrievalApplication.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(provider, credentials); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentRetrievalApplication {\n"); + + sb.append(" provider: ").append(toIndentedString(provider)).append("\n"); + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalResult.java new file mode 100644 index 0000000..17cf3d5 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalResult.java @@ -0,0 +1,99 @@ +package com.repoachiever.model; + +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentRetrievalResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentRetrievalResult implements Serializable { + private @Valid List locations = new ArrayList<>(); + + /** + **/ + public ContentRetrievalResult locations(List locations) { + this.locations = locations; + return this; + } + + + @JsonProperty("locations") + @NotNull + public List getLocations() { + return locations; + } + + @JsonProperty("locations") + public void setLocations(List locations) { + this.locations = locations; + } + + public ContentRetrievalResult addLocationsItem(String locationsItem) { + if (this.locations == null) { + this.locations = new ArrayList<>(); + } + + this.locations.add(locationsItem); + return this; + } + + public ContentRetrievalResult removeLocationsItem(String locationsItem) { + if (locationsItem != null && this.locations != null) { + this.locations.remove(locationsItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentRetrievalResult contentRetrievalResult = (ContentRetrievalResult) o; + return Objects.equals(this.locations, contentRetrievalResult.locations); + } + + @Override + public int hashCode() { + return Objects.hash(locations); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentRetrievalResult {\n"); + + sb.append(" locations: ").append(toIndentedString(locations)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplication.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplication.java new file mode 100644 index 0000000..5e5cbc0 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplication.java @@ -0,0 +1,105 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentStateApplication") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentStateApplication implements Serializable { + private @Valid Provider provider; + private @Valid CredentialsFieldsFull credentials; + + /** + **/ + public ContentStateApplication provider(Provider provider) { + this.provider = provider; + return this; + } + + + @JsonProperty("provider") + @NotNull + public Provider getProvider() { + return provider; + } + + @JsonProperty("provider") + public void setProvider(Provider provider) { + this.provider = provider; + } + + /** + **/ + public ContentStateApplication credentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + return this; + } + + + @JsonProperty("credentials") + @NotNull + public CredentialsFieldsFull getCredentials() { + return credentials; + } + + @JsonProperty("credentials") + public void setCredentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentStateApplication contentStateApplication = (ContentStateApplication) o; + return Objects.equals(this.provider, contentStateApplication.provider) && + Objects.equals(this.credentials, contentStateApplication.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(provider, credentials); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentStateApplication {\n"); + + sb.append(" provider: ").append(toIndentedString(provider)).append("\n"); + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplicationResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplicationResult.java new file mode 100644 index 0000000..94f9f7d --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplicationResult.java @@ -0,0 +1,81 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentStateApplicationResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentStateApplicationResult implements Serializable { + private @Valid String hash; + + /** + **/ + public ContentStateApplicationResult hash(String hash) { + this.hash = hash; + return this; + } + + + @JsonProperty("hash") + @NotNull + public String getHash() { + return hash; + } + + @JsonProperty("hash") + public void setHash(String hash) { + this.hash = hash; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentStateApplicationResult contentStateApplicationResult = (ContentStateApplicationResult) o; + return Objects.equals(this.hash, contentStateApplicationResult.hash); + } + + @Override + public int hashCode() { + return Objects.hash(hash); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentStateApplicationResult {\n"); + + sb.append(" hash: ").append(toIndentedString(hash)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentWithdrawal.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentWithdrawal.java new file mode 100644 index 0000000..af1c0a7 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentWithdrawal.java @@ -0,0 +1,105 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsFull; +import com.repoachiever.model.Provider; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ContentWithdrawal") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ContentWithdrawal implements Serializable { + private @Valid Provider provider; + private @Valid CredentialsFieldsFull credentials; + + /** + **/ + public ContentWithdrawal provider(Provider provider) { + this.provider = provider; + return this; + } + + + @JsonProperty("provider") + @NotNull + public Provider getProvider() { + return provider; + } + + @JsonProperty("provider") + public void setProvider(Provider provider) { + this.provider = provider; + } + + /** + **/ + public ContentWithdrawal credentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + return this; + } + + + @JsonProperty("credentials") + @NotNull + public CredentialsFieldsFull getCredentials() { + return credentials; + } + + @JsonProperty("credentials") + public void setCredentials(CredentialsFieldsFull credentials) { + this.credentials = credentials; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ContentWithdrawal contentWithdrawal = (ContentWithdrawal) o; + return Objects.equals(this.provider, contentWithdrawal.provider) && + Objects.equals(this.credentials, contentWithdrawal.credentials); + } + + @Override + public int hashCode() { + return Objects.hash(provider, credentials); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ContentWithdrawal {\n"); + + sb.append(" provider: ").append(toIndentedString(provider)).append("\n"); + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java new file mode 100644 index 0000000..45dbb53 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java @@ -0,0 +1,82 @@ +package com.repoachiever.model; + +import com.repoachiever.model.GitGitHubCredentials; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("CredentialsFieldsExternal") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class CredentialsFieldsExternal implements Serializable { + private @Valid String token; + + /** + **/ + public CredentialsFieldsExternal token(String token) { + this.token = token; + return this; + } + + + @JsonProperty("token") + @NotNull + public String getToken() { + return token; + } + + @JsonProperty("token") + public void setToken(String token) { + this.token = token; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CredentialsFieldsExternal credentialsFieldsExternal = (CredentialsFieldsExternal) o; + return Objects.equals(this.token, credentialsFieldsExternal.token); + } + + @Override + public int hashCode() { + return Objects.hash(token); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CredentialsFieldsExternal {\n"); + + sb.append(" token: ").append(toIndentedString(token)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsFull.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsFull.java new file mode 100644 index 0000000..b054b75 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsFull.java @@ -0,0 +1,104 @@ +package com.repoachiever.model; + +import com.repoachiever.model.CredentialsFieldsExternal; +import com.repoachiever.model.CredentialsFieldsInternal; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("CredentialsFieldsFull") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class CredentialsFieldsFull implements Serializable { + private @Valid CredentialsFieldsInternal internal; + private @Valid CredentialsFieldsExternal external; + + /** + **/ + public CredentialsFieldsFull internal(CredentialsFieldsInternal internal) { + this.internal = internal; + return this; + } + + + @JsonProperty("internal") + @NotNull + public CredentialsFieldsInternal getInternal() { + return internal; + } + + @JsonProperty("internal") + public void setInternal(CredentialsFieldsInternal internal) { + this.internal = internal; + } + + /** + **/ + public CredentialsFieldsFull external(CredentialsFieldsExternal external) { + this.external = external; + return this; + } + + + @JsonProperty("external") + public CredentialsFieldsExternal getExternal() { + return external; + } + + @JsonProperty("external") + public void setExternal(CredentialsFieldsExternal external) { + this.external = external; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CredentialsFieldsFull credentialsFieldsFull = (CredentialsFieldsFull) o; + return Objects.equals(this.internal, credentialsFieldsFull.internal) && + Objects.equals(this.external, credentialsFieldsFull.external); + } + + @Override + public int hashCode() { + return Objects.hash(internal, external); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CredentialsFieldsFull {\n"); + + sb.append(" internal: ").append(toIndentedString(internal)).append("\n"); + sb.append(" external: ").append(toIndentedString(external)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java new file mode 100644 index 0000000..d49c4a7 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java @@ -0,0 +1,80 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("CredentialsFieldsInternal") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class CredentialsFieldsInternal implements Serializable { + private @Valid Integer id; + + /** + **/ + public CredentialsFieldsInternal id(Integer id) { + this.id = id; + return this; + } + + + @JsonProperty("id") + public Integer getId() { + return id; + } + + @JsonProperty("id") + public void setId(Integer id) { + this.id = id; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CredentialsFieldsInternal credentialsFieldsInternal = (CredentialsFieldsInternal) o; + return Objects.equals(this.id, credentialsFieldsInternal.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CredentialsFieldsInternal {\n"); + + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/GitGitHubCredentials.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/GitGitHubCredentials.java new file mode 100644 index 0000000..ea5b5d0 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/GitGitHubCredentials.java @@ -0,0 +1,81 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("GitGitHubCredentials") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class GitGitHubCredentials implements Serializable { + private @Valid String token; + + /** + **/ + public GitGitHubCredentials token(String token) { + this.token = token; + return this; + } + + + @JsonProperty("token") + @NotNull + public String getToken() { + return token; + } + + @JsonProperty("token") + public void setToken(String token) { + this.token = token; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GitGitHubCredentials gitGitHubCredentials = (GitGitHubCredentials) o; + return Objects.equals(this.token, gitGitHubCredentials.token); + } + + @Override + public int hashCode() { + return Objects.hash(token); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class GitGitHubCredentials {\n"); + + sb.append(" token: ").append(toIndentedString(token)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckResult.java new file mode 100644 index 0000000..5092b48 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckResult.java @@ -0,0 +1,123 @@ +package com.repoachiever.model; + +import com.repoachiever.model.HealthCheckStatus; +import com.repoachiever.model.HealthCheckUnit; +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("HealthCheckResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class HealthCheckResult implements Serializable { + private @Valid HealthCheckStatus status; + private @Valid List checks = new ArrayList<>(); + + /** + **/ + public HealthCheckResult status(HealthCheckStatus status) { + this.status = status; + return this; + } + + + @JsonProperty("status") + @NotNull + public HealthCheckStatus getStatus() { + return status; + } + + @JsonProperty("status") + public void setStatus(HealthCheckStatus status) { + this.status = status; + } + + /** + **/ + public HealthCheckResult checks(List checks) { + this.checks = checks; + return this; + } + + + @JsonProperty("checks") + @NotNull + public List getChecks() { + return checks; + } + + @JsonProperty("checks") + public void setChecks(List checks) { + this.checks = checks; + } + + public HealthCheckResult addChecksItem(HealthCheckUnit checksItem) { + if (this.checks == null) { + this.checks = new ArrayList<>(); + } + + this.checks.add(checksItem); + return this; + } + + public HealthCheckResult removeChecksItem(HealthCheckUnit checksItem) { + if (checksItem != null && this.checks != null) { + this.checks.remove(checksItem); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HealthCheckResult healthCheckResult = (HealthCheckResult) o; + return Objects.equals(this.status, healthCheckResult.status) && + Objects.equals(this.checks, healthCheckResult.checks); + } + + @Override + public int hashCode() { + return Objects.hash(status, checks); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class HealthCheckResult {\n"); + + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append(" checks: ").append(toIndentedString(checks)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckStatus.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckStatus.java new file mode 100644 index 0000000..c4f7833 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckStatus.java @@ -0,0 +1,57 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets HealthCheckStatus + */ +public enum HealthCheckStatus { + + UP("UP"), + + DOWN("DOWN"); + + private String value; + + HealthCheckStatus(String value) { + this.value = value; + } + + /** + * Convert a String into String, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static HealthCheckStatus fromString(String s) { + for (HealthCheckStatus b : HealthCheckStatus.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected string value '" + s + "'"); + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static HealthCheckStatus fromValue(String value) { + for (HealthCheckStatus b : HealthCheckStatus.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckUnit.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckUnit.java new file mode 100644 index 0000000..0188c87 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckUnit.java @@ -0,0 +1,104 @@ +package com.repoachiever.model; + +import com.repoachiever.model.HealthCheckStatus; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("HealthCheckUnit") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class HealthCheckUnit implements Serializable { + private @Valid String name; + private @Valid HealthCheckStatus status; + + /** + **/ + public HealthCheckUnit name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public HealthCheckUnit status(HealthCheckStatus status) { + this.status = status; + return this; + } + + + @JsonProperty("status") + @NotNull + public HealthCheckStatus getStatus() { + return status; + } + + @JsonProperty("status") + public void setStatus(HealthCheckStatus status) { + this.status = status; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HealthCheckUnit healthCheckUnit = (HealthCheckUnit) o; + return Objects.equals(this.name, healthCheckUnit.name) && + Objects.equals(this.status, healthCheckUnit.status); + } + + @Override + public int hashCode() { + return Objects.hash(name, status); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class HealthCheckUnit {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/Provider.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/Provider.java new file mode 100644 index 0000000..4f6b4a0 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/Provider.java @@ -0,0 +1,57 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets Provider + */ +public enum Provider { + + LOCAL("git-local"), + + GITHUB("git-github"); + + private String value; + + Provider(String value) { + this.value = value; + } + + /** + * Convert a String into String, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static Provider fromString(String s) { + for (Provider b : Provider.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected string value '" + s + "'"); + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static Provider fromValue(String value) { + for (Provider b : Provider.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckApplication.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckApplication.java new file mode 100644 index 0000000..6fd2623 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckApplication.java @@ -0,0 +1,80 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ReadinessCheckApplication") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ReadinessCheckApplication implements Serializable { + private @Valid Object test; + + /** + **/ + public ReadinessCheckApplication test(Object test) { + this.test = test; + return this; + } + + + @JsonProperty("test") + public Object getTest() { + return test; + } + + @JsonProperty("test") + public void setTest(Object test) { + this.test = test; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadinessCheckApplication readinessCheckApplication = (ReadinessCheckApplication) o; + return Objects.equals(this.test, readinessCheckApplication.test); + } + + @Override + public int hashCode() { + return Objects.hash(test); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ReadinessCheckApplication {\n"); + + sb.append(" test: ").append(toIndentedString(test)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckResult.java new file mode 100644 index 0000000..4f35056 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckResult.java @@ -0,0 +1,126 @@ +package com.repoachiever.model; + +import com.repoachiever.model.ReadinessCheckStatus; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ReadinessCheckResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ReadinessCheckResult implements Serializable { + private @Valid String name; + private @Valid ReadinessCheckStatus status; + private @Valid Object data; + + /** + **/ + public ReadinessCheckResult name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public ReadinessCheckResult status(ReadinessCheckStatus status) { + this.status = status; + return this; + } + + + @JsonProperty("status") + @NotNull + public ReadinessCheckStatus getStatus() { + return status; + } + + @JsonProperty("status") + public void setStatus(ReadinessCheckStatus status) { + this.status = status; + } + + /** + **/ + public ReadinessCheckResult data(Object data) { + this.data = data; + return this; + } + + + @JsonProperty("data") + @NotNull + public Object getData() { + return data; + } + + @JsonProperty("data") + public void setData(Object data) { + this.data = data; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadinessCheckResult readinessCheckResult = (ReadinessCheckResult) o; + return Objects.equals(this.name, readinessCheckResult.name) && + Objects.equals(this.status, readinessCheckResult.status) && + Objects.equals(this.data, readinessCheckResult.data); + } + + @Override + public int hashCode() { + return Objects.hash(name, status, data); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ReadinessCheckResult {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append(" data: ").append(toIndentedString(data)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckStatus.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckStatus.java new file mode 100644 index 0000000..7770414 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckStatus.java @@ -0,0 +1,57 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets ReadinessCheckStatus + */ +public enum ReadinessCheckStatus { + + UP("UP"), + + DOWN("DOWN"); + + private String value; + + ReadinessCheckStatus(String value) { + this.value = value; + } + + /** + * Convert a String into String, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static ReadinessCheckStatus fromString(String s) { + for (ReadinessCheckStatus b : ReadinessCheckStatus.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected string value '" + s + "'"); + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static ReadinessCheckStatus fromValue(String value) { + for (ReadinessCheckStatus b : ReadinessCheckStatus.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckUnit.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckUnit.java new file mode 100644 index 0000000..6c94db2 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckUnit.java @@ -0,0 +1,104 @@ +package com.repoachiever.model; + +import com.repoachiever.model.ReadinessCheckStatus; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("ReadinessCheckUnit") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class ReadinessCheckUnit implements Serializable { + private @Valid String name; + private @Valid ReadinessCheckStatus status; + + /** + **/ + public ReadinessCheckUnit name(String name) { + this.name = name; + return this; + } + + + @JsonProperty("name") + @NotNull + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + /** + **/ + public ReadinessCheckUnit status(ReadinessCheckStatus status) { + this.status = status; + return this; + } + + + @JsonProperty("status") + @NotNull + public ReadinessCheckStatus getStatus() { + return status; + } + + @JsonProperty("status") + public void setStatus(ReadinessCheckStatus status) { + this.status = status; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadinessCheckUnit readinessCheckUnit = (ReadinessCheckUnit) o; + return Objects.equals(this.name, readinessCheckUnit.name) && + Objects.equals(this.status, readinessCheckUnit.status); + } + + @Override + public int hashCode() { + return Objects.hash(name, status); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ReadinessCheckUnit {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java new file mode 100644 index 0000000..428db14 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java @@ -0,0 +1,103 @@ +package com.repoachiever.model; + +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("VersionExternalApiInfoResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class VersionExternalApiInfoResult implements Serializable { + private @Valid String version; + private @Valid String hash; + + /** + **/ + public VersionExternalApiInfoResult version(String version) { + this.version = version; + return this; + } + + + @JsonProperty("version") + @NotNull + public String getVersion() { + return version; + } + + @JsonProperty("version") + public void setVersion(String version) { + this.version = version; + } + + /** + **/ + public VersionExternalApiInfoResult hash(String hash) { + this.hash = hash; + return this; + } + + + @JsonProperty("hash") + @NotNull + public String getHash() { + return hash; + } + + @JsonProperty("hash") + public void setHash(String hash) { + this.hash = hash; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionExternalApiInfoResult versionExternalApiInfoResult = (VersionExternalApiInfoResult) o; + return Objects.equals(this.version, versionExternalApiInfoResult.version) && + Objects.equals(this.hash, versionExternalApiInfoResult.hash); + } + + @Override + public int hashCode() { + return Objects.hash(version, hash); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VersionExternalApiInfoResult {\n"); + + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" hash: ").append(toIndentedString(hash)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionInfoResult.java b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionInfoResult.java new file mode 100644 index 0000000..ce5829e --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionInfoResult.java @@ -0,0 +1,81 @@ +package com.repoachiever.model; + +import com.repoachiever.model.VersionExternalApiInfoResult; +import java.io.Serializable; +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; + + + +@JsonTypeName("VersionInfoResult") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-05-12T16:25:41.890330+02:00[Europe/Warsaw]")@lombok.Data @lombok.NoArgsConstructor @lombok.AllArgsConstructor(staticName = "of") + +public class VersionInfoResult implements Serializable { + private @Valid VersionExternalApiInfoResult externalApi; + + /** + **/ + public VersionInfoResult externalApi(VersionExternalApiInfoResult externalApi) { + this.externalApi = externalApi; + return this; + } + + + @JsonProperty("externalApi") + public VersionExternalApiInfoResult getExternalApi() { + return externalApi; + } + + @JsonProperty("externalApi") + public void setExternalApi(VersionExternalApiInfoResult externalApi) { + this.externalApi = externalApi; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionInfoResult versionInfoResult = (VersionInfoResult) o; + return Objects.equals(this.externalApi, versionInfoResult.externalApi); + } + + @Override + public int hashCode() { + return Objects.hash(externalApi); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VersionInfoResult {\n"); + + sb.append(" externalApi: ").append(toIndentedString(externalApi)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + +} + diff --git a/api-server/target/generated-sources/openapi/src/main/resources/META-INF/openapi.yaml b/api-server/target/generated-sources/openapi/src/main/resources/META-INF/openapi.yaml new file mode 100644 index 0000000..1e89526 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/resources/META-INF/openapi.yaml @@ -0,0 +1,465 @@ +openapi: 3.0.1 +info: + description: RepoAchiever API Server Open API documentation + title: OpenAPI document of RepoAchiever API Server + version: "1.0" +servers: +- url: / +tags: +- description: Contains all endpoints related to operations on processed content. + name: ContentResource +- description: Contains all endpoints related to state processing. + name: StateResource +- description: Contains all endpoints related to general info of API Server. + name: InfoResource +- description: Contains all endpoints related to general API Server health information. + name: HealthResource +paths: + /v1/content: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentRetrievalApplication' + description: Content retrieval application + required: true + responses: + "204": + content: + application/json: + schema: + $ref: '#/components/schemas/ContentRetrievalResult' + description: A list of all available content + tags: + - ContentResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: ContentResource + /v1/content/apply: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentApplication' + description: Content configuration application + required: true + responses: + "204": + description: Given content configuration was successfully applied + "400": + description: Given content configuration was not applied + tags: + - ContentResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: ContentResource + /v1/content/withdraw: + delete: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentWithdrawal' + description: Content withdraw application. Does not remove persisted content. + required: true + responses: + "204": + description: Given content configuration was successfully withdrawn + "400": + description: Given content configuration was not withdrawn + tags: + - ContentResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: ContentResource + /v1/content/download: + get: + parameters: + - description: Name of content location to be downloaded + explode: true + in: query + name: location + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/octet-stream: + schema: + format: binary + type: string + description: A content was successfully retrieved + tags: + - ContentResource + x-accepts: application/octet-stream + x-tags: + - tag: ContentResource + /v1/content/clean: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentCleanup' + description: Content configuration application + required: true + responses: + "201": + description: Content with the given configuration was successfully deleted + "400": + description: Content with the given configuration was not deleted + tags: + - ContentResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: ContentResource + /v1/state/content: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentStateApplication' + description: Given content state key + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/ContentStateApplicationResult' + description: Content state hash is retrieved successfully + tags: + - StateResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: StateResource + /v1/info/version: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/VersionInfoResult' + description: General information about running API Server + tags: + - InfoResource + x-accepts: application/json + x-tags: + - tag: InfoResource + /v1/info/cluster: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ClusterInfoResult' + description: General information about running clusters + tags: + - InfoResource + x-accepts: application/json + x-tags: + - tag: InfoResource + /v1/info/telemetry: + get: + responses: + "200": + content: + text/plain: + schema: + type: string + description: A set of Prometheus samples used by Grafana instance + tags: + - InfoResource + x-accepts: text/plain + x-tags: + - tag: InfoResource + /v1/health: + get: + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResult' + description: General health information about running API Server + tags: + - HealthResource + x-accepts: application/json + x-tags: + - tag: HealthResource + /v1/readiness: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ReadinessCheckApplication' + description: Check if API Server is ready to serve for the given user + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ReadinessCheckResult' + description: General health information about running API Server + tags: + - HealthResource + x-content-type: application/json + x-accepts: application/json + x-tags: + - tag: HealthResource +components: + schemas: + Provider: + enum: + - git-local + - git-github + type: string + CredentialsFieldsFull: + example: + internal: + id: 0 + external: null + properties: + internal: + $ref: '#/components/schemas/CredentialsFieldsInternal' + external: + $ref: '#/components/schemas/CredentialsFieldsExternal' + required: + - internal + CredentialsFieldsInternal: + example: + id: 0 + properties: + id: + type: integer + CredentialsFieldsExternal: + anyOf: + - $ref: '#/components/schemas/GitGitHubCredentials' + GitGitHubCredentials: + properties: + token: + type: string + required: + - token + ContentRetrievalApplication: + example: + provider: null + credentials: + internal: + id: 0 + external: null + properties: + provider: + $ref: '#/components/schemas/Provider' + credentials: + $ref: '#/components/schemas/CredentialsFieldsFull' + required: + - credentials + - provider + ContentRetrievalResult: + example: + locations: + - locations + - locations + properties: + locations: + items: + type: string + type: array + required: + - locations + ContentApplication: + example: + provider: null + credentials: + internal: + id: 0 + external: null + locations: + - locations + - locations + properties: + locations: + items: + type: string + type: array + provider: + $ref: '#/components/schemas/Provider' + credentials: + $ref: '#/components/schemas/CredentialsFieldsFull' + required: + - credentials + - locations + - provider + ContentWithdrawal: + example: + provider: null + credentials: + internal: + id: 0 + external: null + properties: + provider: + $ref: '#/components/schemas/Provider' + credentials: + $ref: '#/components/schemas/CredentialsFieldsFull' + required: + - credentials + - provider + ContentCleanup: + example: + credentials: + internal: + id: 0 + external: null + properties: + credentials: + $ref: '#/components/schemas/CredentialsFieldsFull' + required: + - credentials + ContentStateApplication: + example: + provider: null + credentials: + internal: + id: 0 + external: null + properties: + provider: + $ref: '#/components/schemas/Provider' + credentials: + $ref: '#/components/schemas/CredentialsFieldsFull' + required: + - credentials + - provider + ContentStateApplicationResult: + example: + hash: hash + properties: + hash: + type: string + required: + - hash + VersionInfoResult: + example: + externalApi: + version: version + hash: hash + properties: + externalApi: + $ref: '#/components/schemas/VersionExternalApiInfoResult' + VersionExternalApiInfoResult: + example: + version: version + hash: hash + properties: + version: + type: string + hash: + type: string + required: + - hash + - version + ClusterInfoResult: + items: + $ref: '#/components/schemas/ClusterInfoUnit' + type: array + ClusterInfoUnit: + example: + name: name + health: true + workers: 0 + properties: + name: + type: string + health: + type: boolean + workers: + type: integer + required: + - name + HealthCheckResult: + example: + checks: + - name: name + status: null + - name: name + status: null + status: null + properties: + status: + $ref: '#/components/schemas/HealthCheckStatus' + checks: + items: + $ref: '#/components/schemas/HealthCheckUnit' + type: array + required: + - checks + - status + HealthCheckUnit: + example: + name: name + status: null + properties: + name: + type: string + status: + $ref: '#/components/schemas/HealthCheckStatus' + required: + - name + - status + HealthCheckStatus: + enum: + - UP + - DOWN + type: string + ReadinessCheckApplication: + example: + test: "{}" + properties: + test: + type: object + ReadinessCheckResult: + example: + data: "{}" + name: name + status: null + properties: + name: + type: string + status: + $ref: '#/components/schemas/ReadinessCheckStatus' + data: + type: object + required: + - data + - name + - status + ReadinessCheckUnit: + properties: + name: + type: string + status: + $ref: '#/components/schemas/ReadinessCheckStatus' + required: + - name + - status + ReadinessCheckStatus: + enum: + - UP + - DOWN + type: string diff --git a/api-server/target/generated-sources/openapi/src/main/resources/application.properties b/api-server/target/generated-sources/openapi/src/main/resources/application.properties new file mode 100644 index 0000000..83b16e9 --- /dev/null +++ b/api-server/target/generated-sources/openapi/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# Configuration file +# key = value + +mp.openapi.scan.disable=true + diff --git a/api-server/target/maven-archiver/pom.properties b/api-server/target/maven-archiver/pom.properties new file mode 100644 index 0000000..b2fcf44 --- /dev/null +++ b/api-server/target/maven-archiver/pom.properties @@ -0,0 +1,4 @@ +#Created by Apache Maven 3.9.6 +artifactId=api-server +groupId=com.repoachiever +version=1.0-SNAPSHOT diff --git a/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..63ea8a0 --- /dev/null +++ b/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,210 @@ +com/repoachiever/model/GitGitHubCredentials.class +com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.class +com/repoachiever/repository/facade/RepositoryFacade.class +com/repoachiever/model/ContentCleanup.class +com/repoachiever/exception/ContentFileNotFoundException.class +com/repoachiever/dto/RepositoryContentUnitDto.class +com/repoachiever/entity/common/ClusterContextEntity$Metadata.class +com/repoachiever/api/InfoResourceApi.class +com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.class +com/repoachiever/service/workspace/WorkspaceService$1.class +com/repoachiever/exception/RepositoryContentApplicationFailureException.class +com/repoachiever/model/HealthCheckUnit.class +com/repoachiever/dto/CommandExecutorOutputDto.class +com/repoachiever/model/HealthCheckResult.class +com/repoachiever/exception/CommandExecutorException.class +com/repoachiever/exception/QueryExecutionFailureException.class +com/repoachiever/entity/common/ClusterContextEntity$Resource$Cluster.class +com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.class +com/repoachiever/service/cluster/resource/ClusterCommunicationResource.class +com/repoachiever/model/ContentStateApplication.class +com/repoachiever/exception/NodeExporterDeploymentFailureException.class +com/repoachiever/resource/ContentResource.class +com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.class +com/repoachiever/resource/HealthResource.class +com/repoachiever/entity/common/ClusterContextEntity$Filter.class +com/repoachiever/entity/common/ConfigEntity$Connection.class +com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper$3.class +com/repoachiever/exception/DockerNetworkRemoveFailureException.class +com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.class +com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper$1.class +com/repoachiever/entity/common/ConfigEntity$Diagnostics.class +com/repoachiever/entity/repository/ProviderEntity.class +com/repoachiever/entity/common/ConfigEntity.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$3.class +com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.class +com/repoachiever/exception/ClusterApplicationTimeoutException.class +com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter$1.class +com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper$2.class +com/repoachiever/entity/common/ClusterContextEntity$Communication.class +com/repoachiever/model/CredentialsFieldsFull.class +com/repoachiever/exception/RepositoryOperationFailureException.class +com/repoachiever/model/ReadinessCheckUnit.class +com/repoachiever/entity/common/ClusterContextEntity$Content.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$3$1.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$2.class +com/repoachiever/model/ContentRetrievalApplication.class +com/repoachiever/api/ContentResourceApi.class +com/repoachiever/exception/ContentFileWriteFailureException.class +com/repoachiever/exception/QueryEmptyResultException.class +com/repoachiever/entity/common/ClusterContextEntity$Resource.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$1.class +com/repoachiever/entity/common/ClusterContextEntity$Resource$Worker.class +com/repoachiever/repository/common/RepositoryConfigurationHelper.class +com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper$1.class +com/repoachiever/entity/common/ClusterContextEntity$Service$Provider.class +com/repoachiever/entity/common/ConfigEntity$Diagnostics$Grafana.class +com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.class +com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.class +com/repoachiever/model/HealthCheckStatus.class +com/repoachiever/service/command/prometheus/PrometheusDeployCommandService$1.class +com/repoachiever/service/config/ConfigService.class +com/repoachiever/model/ReadinessCheckApplication.class +com/repoachiever/model/ContentApplication.class +com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.class +com/repoachiever/exception/ClusterOperationFailureException.class +com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$3$2.class +com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper$1.class +com/repoachiever/model/Provider.class +com/repoachiever/exception/MetadataFileNotFoundException.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$2$1.class +com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.class +com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.class +com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.class +com/repoachiever/entity/common/ConfigEntity$Resource$Cluster.class +com/repoachiever/entity/common/ConfigEntity$Database.class +com/repoachiever/model/VersionExternalApiInfoResult.class +com/repoachiever/exception/ClusterDestructionFailureException.class +com/repoachiever/api/HealthResourceApi.class +com/repoachiever/dto/ClusterAllocationDto.class +com/repoachiever/converter/ClusterContextToJsonConverter.class +com/repoachiever/service/vendor/VendorFacade$1.class +com/repoachiever/exception/ContentApplicationRetrievalFailureException.class +com/repoachiever/service/workspace/facade/WorkspaceFacade.class +com/repoachiever/exception/CommunicationConfigurationFailureException.class +com/repoachiever/entity/common/ClusterContextEntity$Service.class +com/repoachiever/service/telemetry/TelemetryService.class +com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.class +com/repoachiever/entity/common/ConfigEntity$Content.class +com/repoachiever/service/integration/state/StateConfigService.class +com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService$1.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.class +com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.class +com/repoachiever/service/command/grafana/GrafanaDeployCommandService$1.class +com/repoachiever/exception/ConfigValidationException.class +com/repoachiever/exception/CredentialsFieldIsNotValidException.class +com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.class +com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.class +com/repoachiever/exception/RepositoryContentDestructionFailureException.class +com/repoachiever/service/telemetry/binding/TelemetryBinding.class +com/repoachiever/exception/DockerInspectRemovalFailureException.class +com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.class +com/repoachiever/resource/StateResource.class +com/repoachiever/service/workspace/WorkspaceService.class +com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper$3.class +com/repoachiever/entity/common/MetadataFileEntity.class +com/repoachiever/service/workspace/facade/WorkspaceFacade$1.class +com/repoachiever/entity/common/ConfigEntity$Resource$Worker.class +com/repoachiever/service/vendor/common/VendorConfigurationHelper.class +com/repoachiever/service/integration/http/HttpServerConfigService.class +com/repoachiever/exception/ClusterWithdrawalFailureException.class +com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.class +com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult$1.class +com/repoachiever/repository/common/RepositoryConfigurationHelper$1.class +com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.class +com/repoachiever/entity/common/ClusterContextEntity$Service$Credentials.class +com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.class +com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.class +com/repoachiever/exception/LocationsFieldIsNotValidException.class +com/repoachiever/service/command/common/CommandConfigurationHelper.class +com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.class +com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService$1.class +com/repoachiever/resource/common/ResourceConfigurationHelper$1.class +META-INF/panache-archive.marker +com/repoachiever/model/ReadinessCheckResult.class +com/repoachiever/service/config/ConfigService$1.class +com/repoachiever/service/healthcheck/health/HealthCheckService.class +com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.class +com/repoachiever/exception/CredentialsAreNotValidException.class +com/repoachiever/entity/common/ConfigEntity$Communication.class +com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService$1.class +com/repoachiever/model/CredentialsFieldsInternal.class +com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.class +META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat +com/repoachiever/RestResourceRoot.class +com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.class +com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.class +com/repoachiever/entity/repository/ConfigEntity.class +com/repoachiever/exception/DockerIsNotAvailableException.class +com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper$2.class +com/repoachiever/service/vendor/VendorFacade.class +com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.class +com/repoachiever/exception/PrometheusDeploymentFailureException.class +com/repoachiever/model/ClusterInfoUnit.class +com/repoachiever/entity/repository/ContentEntity.class +com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper$2.class +com/repoachiever/service/command/grafana/GrafanaDeployCommandService.class +com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.class +com/repoachiever/entity/common/PropertiesEntity.class +com/repoachiever/model/ContentWithdrawal.class +com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper$1.class +com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.class +com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.class +com/repoachiever/entity/common/ConfigEntity$Resource.class +com/repoachiever/exception/DockerNetworkCreateFailureException.class +com/repoachiever/entity/common/ConfigEntity$Diagnostics$Prometheus.class +com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper$3.class +com/repoachiever/repository/ContentRepository.class +com/repoachiever/model/ReadinessCheckStatus.class +com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.class +com/repoachiever/service/cluster/common/ClusterConfigurationHelper.class +com/repoachiever/repository/executor/RepositoryExecutor.class +com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService$1.class +com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.class +com/repoachiever/service/vendor/git/github/GitGitHubVendorService.class +com/repoachiever/repository/ConfigRepository.class +com/repoachiever/entity/repository/SecretEntity.class +com/repoachiever/service/state/StateService.class +com/repoachiever/model/VersionInfoResult.class +com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.class +com/repoachiever/service/executor/CommandExecutorService.class +com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.class +com/repoachiever/api/StateResourceApi.class +com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.class +com/repoachiever/model/CredentialsFieldsExternal.class +com/repoachiever/exception/ContentFileRemovalFailureException.class +com/repoachiever/exception/ClusterApplicationFailureException.class +com/repoachiever/model/ContentStateApplicationResult.class +com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService$1.class +com/repoachiever/entity/common/ConfigEntity$Diagnostics$Metrics.class +com/repoachiever/model/ContentRetrievalResult.class +com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.class +com/repoachiever/resource/common/ResourceConfigurationHelper.class +com/repoachiever/resource/InfoResource.class +com/repoachiever/exception/TelemetryOperationFailureException.class +com/repoachiever/service/communication/cluster/IClusterCommunicationService.class +com/repoachiever/resource/communication/ApiServerCommunicationResource.class +com/repoachiever/logging/FatalAppender.class +com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService$1.class +com/repoachiever/exception/ClusterRecreationFailureException.class +com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.class +com/repoachiever/repository/ProviderRepository.class +com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$1$2.class +com/repoachiever/exception/ClusterDeploymentFailureException.class +com/repoachiever/exception/MetadataFileWriteFailureException.class +com/repoachiever/exception/CredentialsConversionException.class +com/repoachiever/service/cluster/ClusterService.class +com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService$1.class +com/repoachiever/exception/ClusterFullDestructionFailureException.class +com/repoachiever/entity/common/ConfigEntity$Diagnostics$NodeExporter.class +com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.class +com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.class +com/repoachiever/service/client/github/IGitHubClientService.class +com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.class +com/repoachiever/service/cluster/facade/ClusterFacade.class +com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.class +com/repoachiever/entity/common/ClusterContextEntity.class +com/repoachiever/repository/SecretRepository.class +com/repoachiever/service/integration/diagnostics/template/TemplateConfigService$1$1.class diff --git a/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..12ced00 --- /dev/null +++ b/api-server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,151 @@ +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/converter/HealthCheckResponseToReadinessCheckResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/DockerInspectRemovalFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsExternal.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentWithdrawal.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/repository/SecretEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/prometheus/PrometheusDeployCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/CredentialsConversionException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckUnit.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/client/github/IGitHubClientService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterDeploymentFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/ConfigRepository.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckStatus.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentDestructionFailureExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/telemetry/TelemetryConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/repository/ConfigEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/healthcheck/ClusterHealthCheckCommunicationService.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentCleanup.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/RepositoryOperationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentApplication.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/ContentResourceApi.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckUnit.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/repository/ProviderEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/ContentRepository.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/RepositoryContentDestructionFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ContentApplicationRetrievalFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/properties/general/GeneralPropertiesConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/QueryEmptyResultException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/communication/apiserver/ApiServerCommunicationConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/cluster/common/ClusterConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ClusterInfoUnit.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/QueryExecutionFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/InfoResource.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/Provider.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/WorkspaceContentDirectoryCreationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/state/StateService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/common/CommandConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/properties/git/GitPropertiesConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/PrometheusDeploymentFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/executor/RepositoryExecutor.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/DiagnosticsConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/InfoResourceApi.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/communication/ApiServerCommunicationResource.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/common/ConfigEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryCreationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/common/MetadataFileEntity.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/RestResourceRoot.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/repository/ContentEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/ProviderRepository.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/communication/registry/RegistryCommunicationConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/converter/ContentProviderToClusterContextProviderConverter.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/workspace/WorkspaceService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryNotFoundException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/NodeExporterDeploymentFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/DockerIsNotAvailableException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentRetrievalApplication.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ContentFileNotFoundException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/GitGitHubCredentials.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/HealthResource.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/cluster/ClusterService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryRemovalFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/common/PropertiesEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/telemetry/TelemetryService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ConfigValidationException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterUnhealthyReapplicationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/common/ResourceConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/CredentialsFieldIsNotValidExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckApplication.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/docker/availability/DockerAvailabilityCheckCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/RepositoryContentApplicationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/converter/ClusterContextToJsonConverter.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterDestructionFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/vendor/common/VendorConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/MetadataFileWriteFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/LocationsFieldIsNotValidExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/DiagnosticsTemplateProcessingFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/cluster/resource/ClusterCommunicationResource.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/RepositoryContentApplicationFailureExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/communication/cluster/topology/ClusterTopologyCommunicationConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/WorkspaceUnitDirectoryPresentException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/common/RepositoryConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/facade/RepositoryFacade.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/http/HttpServerConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/cluster/deploy/ClusterDeployCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/HealthResourceApi.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/workspace/facade/WorkspaceFacade.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/CommandExecutorException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/healthcheck/readiness/ReadinessCheckService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/docker/inspect/remove/DockerInspectRemoveCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/ClusterWithdrawalFailureExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationTimeoutException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplication.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/repository/SecretRepository.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/diagnostics/template/TemplateConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/TelemetryOperationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterRecreationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionInfoResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/LocationsFieldIsNotValidException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterWithdrawalFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/StateResource.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ContentFileRemovalFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/common/NodeExporterConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/ClusterApplicationFailureExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/dto/ClusterAllocationDto.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/client/smallrye/ISmallRyeHealthCheckClientService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/prometheus/common/PrometheusConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/cluster/facade/ClusterFacade.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsFull.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/nodeexporter/NodeExporterDeployCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/cluster/destroy/ClusterDestroyCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/vendor/VendorFacade.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/CredentialsFieldIsNotValidException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/MetadataFileNotFoundException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/VersionExternalApiInfoResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/dto/RepositoryContentUnitDto.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/healthcheck/health/HealthCheckService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/docker/network/create/DockerNetworkCreateCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckStatus.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/docker/network/remove/DockerNetworkRemoveCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/grafana/GrafanaDeployCommandService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ContentFileWriteFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/cluster/common/ClusterConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/WorkspaceUnitDirectoryNotFoundExceptionMapper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/entity/common/ClusterContextEntity.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/config/ConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/telemetry/binding/TelemetryBinding.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterFullDestructionFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/CredentialsFieldsInternal.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterOperationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/DockerNetworkRemoveFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/integration/state/StateConfigService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/vendor/git/github/GitGitHubVendorService.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/DockerNetworkCreateFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/logging/FatalAppender.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/api/StateResourceApi.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/service/command/grafana/common/GrafanaConfigurationHelper.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ApiServerInstanceIsAlreadyRunningException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/CredentialsAreNotValidException.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ReadinessCheckResult.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/ContentStateApplicationResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/exception/ClusterApplicationFailureException.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/converter/ContentCredentialsToClusterContextCredentialsConverter.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/resource/ContentResource.java +/Volumes/Files/java/RepoAchiever/api-server/target/generated-sources/openapi/src/main/java/com/repoachiever/model/HealthCheckResult.java +/Volumes/Files/java/RepoAchiever/api-server/src/main/java/com/repoachiever/mapping/CredentialsAreNotValidExceptionMapper.java diff --git a/api-server/target/quarkus-app/quarkus-app-dependencies.txt b/api-server/target/quarkus-app/quarkus-app-dependencies.txt new file mode 100644 index 0000000..036c801 --- /dev/null +++ b/api-server/target/quarkus-app/quarkus-app-dependencies.txt @@ -0,0 +1,293 @@ +Shell-Command-Executor-Lib:Shell-Command-Executor-Lib::jar:0.5.0-SNAPSHOST +com.aayushatharva.brotli4j:brotli4j::jar:1.7.1 +com.fasterxml.jackson.core:jackson-annotations::jar:2.15.3 +com.fasterxml.jackson.core:jackson-core::jar:2.15.3 +com.fasterxml.jackson.core:jackson-databind::jar:2.15.3 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml::jar:2.15.3 +com.fasterxml.jackson.datatype:jackson-datatype-jdk8::jar:2.15.3 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310::jar:2.15.3 +com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base::jar:2.15.3 +com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider::jar:2.15.3 +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations::jar:2.15.3 +com.fasterxml.jackson.module:jackson-module-parameter-names::jar:2.15.3 +com.fasterxml:classmate::jar:1.5.1 +com.github.ben-manes.caffeine:caffeine::jar:3.1.8 +com.github.java-json-tools:btf::jar:1.3 +com.github.java-json-tools:jackson-coreutils::jar:2.0 +com.github.java-json-tools:json-patch::jar:1.13 +com.github.java-json-tools:msg-simple::jar:1.2 +com.google.errorprone:error_prone_annotations::jar:2.21.1 +com.ibm.async:asyncutil::jar:0.1.0 +com.opencsv:opencsv::jar:5.6 +com.sun.istack:istack-commons-runtime::jar:4.1.2 +commons-beanutils:commons-beanutils::jar:1.9.4 +commons-codec:commons-codec::jar:1.15 +commons-collections:commons-collections::jar:3.2.2 +commons-io:commons-io::jar:2.11.0 +commons-logging:commons-logging::jar:1.2 +io.agroal:agroal-api::jar:2.1 +io.agroal:agroal-narayana::jar:2.1 +io.agroal:agroal-pool::jar:2.1 +io.github.crac:org-crac::jar:0.1.3 +io.micrometer:micrometer-commons::jar:1.11.5 +io.micrometer:micrometer-core::jar:1.11.5 +io.micrometer:micrometer-observation::jar:1.11.5 +io.micrometer:micrometer-registry-prometheus::jar:1.11.5 +io.mvnpm:importmap::jar:1.0.10 +io.netty:netty-buffer::jar:4.1.100.Final +io.netty:netty-codec-dns::jar:4.1.100.Final +io.netty:netty-codec-haproxy::jar:4.1.100.Final +io.netty:netty-codec-http2::jar:4.1.100.Final +io.netty:netty-codec-http::jar:4.1.100.Final +io.netty:netty-codec-socks::jar:4.1.100.Final +io.netty:netty-codec::jar:4.1.100.Final +io.netty:netty-common::jar:4.1.100.Final +io.netty:netty-handler-proxy::jar:4.1.100.Final +io.netty:netty-handler::jar:4.1.100.Final +io.netty:netty-resolver-dns::jar:4.1.100.Final +io.netty:netty-resolver::jar:4.1.100.Final +io.netty:netty-transport-native-unix-common::jar:4.1.100.Final +io.netty:netty-transport::jar:4.1.100.Final +io.pebbletemplates:pebble::jar:3.2.2 +io.prometheus:simpleclient::jar:0.16.0 +io.prometheus:simpleclient_common::jar:0.16.0 +io.prometheus:simpleclient_tracer_common::jar:0.16.0 +io.prometheus:simpleclient_tracer_otel::jar:0.16.0 +io.prometheus:simpleclient_tracer_otel_agent::jar:0.16.0 +io.quarkiverse.jdbc:quarkus-jdbc-sqlite::jar:3.0.7 +io.quarkus.arc:arc-processor::jar:3.4.3 +io.quarkus.arc:arc::jar:3.4.3 +io.quarkus.gizmo:gizmo::jar:1.6.1.Final +io.quarkus.qute:qute-core::jar:3.4.3 +io.quarkus.resteasy.reactive:resteasy-reactive-common-types::jar:3.4.3 +io.quarkus.resteasy.reactive:resteasy-reactive-common::jar:3.4.3 +io.quarkus.resteasy.reactive:resteasy-reactive-jackson::jar:3.4.3 +io.quarkus.resteasy.reactive:resteasy-reactive-vertx::jar:3.4.3 +io.quarkus.resteasy.reactive:resteasy-reactive::jar:3.4.3 +io.quarkus.security:quarkus-security::jar:2.0.2.Final +io.quarkus:quarkus-agroal::jar:3.4.3 +io.quarkus:quarkus-apache-httpclient::jar:3.4.3 +io.quarkus:quarkus-arc-deployment::jar:3.4.3 +io.quarkus:quarkus-arc::jar:3.4.3 +io.quarkus:quarkus-bootstrap-app-model::jar:3.4.3 +io.quarkus:quarkus-bootstrap-core::jar:3.4.3 +io.quarkus:quarkus-bootstrap-runner::jar:3.4.3 +io.quarkus:quarkus-builder::jar:3.4.3 +io.quarkus:quarkus-caffeine::jar:3.4.3 +io.quarkus:quarkus-class-change-agent::jar:3.4.3 +io.quarkus:quarkus-core-deployment::jar:3.4.3 +io.quarkus:quarkus-core::jar:3.4.3 +io.quarkus:quarkus-credentials::jar:3.4.3 +io.quarkus:quarkus-datasource-common::jar:3.4.3 +io.quarkus:quarkus-datasource::jar:3.4.3 +io.quarkus:quarkus-development-mode-spi::jar:3.4.3 +io.quarkus:quarkus-devtools-utilities::jar:3.4.3 +io.quarkus:quarkus-fs-util::jar:0.0.9 +io.quarkus:quarkus-hibernate-orm-panache-common::jar:3.4.3 +io.quarkus:quarkus-hibernate-orm-panache::jar:3.4.3 +io.quarkus:quarkus-hibernate-orm::jar:3.4.3 +io.quarkus:quarkus-hibernate-validator::jar:3.4.3 +io.quarkus:quarkus-jackson::jar:3.4.3 +io.quarkus:quarkus-jaxb::jar:3.4.3 +io.quarkus:quarkus-jaxp::jar:3.4.3 +io.quarkus:quarkus-jsonp::jar:3.4.3 +io.quarkus:quarkus-kubernetes-spi::jar:3.4.3 +io.quarkus:quarkus-liquibase::jar:3.4.3 +io.quarkus:quarkus-micrometer-registry-prometheus::jar:3.4.3 +io.quarkus:quarkus-micrometer::jar:3.4.3 +io.quarkus:quarkus-mutiny-deployment::jar:3.4.3 +io.quarkus:quarkus-mutiny::jar:3.4.3 +io.quarkus:quarkus-narayana-jta::jar:3.4.3 +io.quarkus:quarkus-netty-deployment::jar:3.4.3 +io.quarkus:quarkus-netty::jar:3.4.3 +io.quarkus:quarkus-panache-common::jar:3.4.3 +io.quarkus:quarkus-panache-hibernate-common::jar:3.4.3 +io.quarkus:quarkus-rest-client-config::jar:3.4.3 +io.quarkus:quarkus-rest-client-jackson::jar:3.4.3 +io.quarkus:quarkus-rest-client::jar:3.4.3 +io.quarkus:quarkus-resteasy-common::jar:3.4.3 +io.quarkus:quarkus-resteasy-reactive-common::jar:3.4.3 +io.quarkus:quarkus-resteasy-reactive-jackson-common::jar:3.4.3 +io.quarkus:quarkus-resteasy-reactive-jackson::jar:3.4.3 +io.quarkus:quarkus-resteasy-reactive::jar:3.4.3 +io.quarkus:quarkus-security-runtime-spi::jar:3.4.3 +io.quarkus:quarkus-smallrye-context-propagation-deployment::jar:3.4.3 +io.quarkus:quarkus-smallrye-context-propagation-spi::jar:3.4.3 +io.quarkus:quarkus-smallrye-context-propagation::jar:3.4.3 +io.quarkus:quarkus-smallrye-health::jar:3.4.3 +io.quarkus:quarkus-smallrye-openapi::jar:3.4.3 +io.quarkus:quarkus-swagger-ui::jar:3.4.3 +io.quarkus:quarkus-transaction-annotations::jar:3.4.3 +io.quarkus:quarkus-vertx-deployment::jar:3.4.3 +io.quarkus:quarkus-vertx-http-deployment-spi::jar:3.4.3 +io.quarkus:quarkus-vertx-http-deployment::jar:3.4.3 +io.quarkus:quarkus-vertx-http-dev-console-runtime-spi::jar:3.4.3 +io.quarkus:quarkus-vertx-http-dev-console-spi::jar:3.4.3 +io.quarkus:quarkus-vertx-http-dev-ui-resources::jar:3.4.3 +io.quarkus:quarkus-vertx-http-dev-ui-spi::jar:3.4.3 +io.quarkus:quarkus-vertx-http::jar:3.4.3 +io.quarkus:quarkus-vertx-latebound-mdc-provider::jar:3.4.3 +io.quarkus:quarkus-vertx::jar:3.4.3 +io.quarkus:quarkus-virtual-threads-deployment::jar:3.4.3 +io.quarkus:quarkus-virtual-threads::jar:3.4.3 +io.smallrye.common:smallrye-common-annotation::jar:2.1.2 +io.smallrye.common:smallrye-common-classloader::jar:2.1.0 +io.smallrye.common:smallrye-common-constraint::jar:2.1.2 +io.smallrye.common:smallrye-common-cpu::jar:2.1.0 +io.smallrye.common:smallrye-common-expression::jar:2.1.0 +io.smallrye.common:smallrye-common-function::jar:2.1.0 +io.smallrye.common:smallrye-common-io::jar:2.1.2 +io.smallrye.common:smallrye-common-net::jar:2.1.0 +io.smallrye.common:smallrye-common-os::jar:2.1.2 +io.smallrye.common:smallrye-common-ref::jar:2.1.0 +io.smallrye.common:smallrye-common-vertx-context::jar:2.1.2 +io.smallrye.config:smallrye-config-common::jar:3.3.4 +io.smallrye.config:smallrye-config-core::jar:3.3.4 +io.smallrye.config:smallrye-config-validator::jar:3.3.4 +io.smallrye.config:smallrye-config::jar:3.3.4 +io.smallrye.reactive:mutiny-smallrye-context-propagation::jar:2.3.1 +io.smallrye.reactive:mutiny-zero-flow-adapters::jar:1.0.0 +io.smallrye.reactive:mutiny::jar:2.3.1 +io.smallrye.reactive:smallrye-mutiny-vertx-auth-common::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-bridge-common::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-core::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-runtime::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-uri-template::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-web-common::jar:3.5.0 +io.smallrye.reactive:smallrye-mutiny-vertx-web::jar:3.5.0 +io.smallrye.reactive:smallrye-reactive-converter-api::jar:3.0.0 +io.smallrye.reactive:smallrye-reactive-converter-mutiny::jar:3.0.0 +io.smallrye.reactive:vertx-mutiny-generator::jar:3.5.0 +io.smallrye:jandex::jar:3.1.5 +io.smallrye:smallrye-context-propagation-api::jar:2.1.0 +io.smallrye:smallrye-context-propagation-jta::jar:2.1.0 +io.smallrye:smallrye-context-propagation-storage::jar:2.1.0 +io.smallrye:smallrye-context-propagation::jar:2.1.0 +io.smallrye:smallrye-fault-tolerance-vertx::jar:6.2.6 +io.smallrye:smallrye-health-api::jar:4.0.4 +io.smallrye:smallrye-health-provided-checks::jar:4.0.4 +io.smallrye:smallrye-health::jar:4.0.4 +io.smallrye:smallrye-open-api-core::jar:3.5.2 +io.vertx:vertx-auth-common::jar:4.4.5 +io.vertx:vertx-bridge-common::jar:4.4.5 +io.vertx:vertx-codegen::jar:4.4.4 +io.vertx:vertx-core::jar:4.4.5 +io.vertx:vertx-uri-template::jar:4.4.4 +io.vertx:vertx-web-common::jar:4.4.5 +io.vertx:vertx-web::jar:4.4.5 +jakarta.activation:jakarta.activation-api::jar:2.1.2 +jakarta.annotation:jakarta.annotation-api::jar:2.1.1 +jakarta.ejb:jakarta.ejb-api::jar:4.0.1 +jakarta.el:jakarta.el-api::jar:5.0.0 +jakarta.enterprise:jakarta.enterprise.cdi-api::jar:4.0.1 +jakarta.enterprise:jakarta.enterprise.lang-model::jar:4.0.1 +jakarta.inject:jakarta.inject-api::jar:2.0.1 +jakarta.interceptor:jakarta.interceptor-api::jar:2.1.0 +jakarta.json:jakarta.json-api::jar:2.1.3 +jakarta.persistence:jakarta.persistence-api::jar:3.1.0 +jakarta.resource:jakarta.resource-api::jar:2.0.0 +jakarta.servlet:jakarta.servlet-api::jar:6.0.0 +jakarta.transaction:jakarta.transaction-api::jar:2.0.1 +jakarta.validation:jakarta.validation-api::jar:3.0.2 +jakarta.ws.rs:jakarta.ws.rs-api::jar:3.1.0 +jakarta.xml.bind:jakarta.xml.bind-api::jar:4.0.1 +net.bytebuddy:byte-buddy::jar:1.14.9 +org.aesh:aesh::jar:2.7 +org.aesh:readline::jar:2.4 +org.antlr:antlr4-runtime::jar:4.10.1 +org.apache.commons:commons-collections4::jar:4.4 +org.apache.commons:commons-lang3::jar:3.14.0 +org.apache.commons:commons-text::jar:1.9 +org.apache.httpcomponents:httpasyncclient::jar:4.1.5 +org.apache.httpcomponents:httpclient::jar:4.5.14 +org.apache.httpcomponents:httpcore-nio::jar:4.4.16 +org.apache.httpcomponents:httpcore::jar:4.4.16 +org.apache.logging.log4j:log4j-api::jar:2.23.1 +org.apache.logging.log4j:log4j-core::jar:2.23.1 +org.apiguardian:apiguardian-api::jar:1.1.2 +org.eclipse.angus:angus-activation::jar:2.0.1 +org.eclipse.microprofile.config:microprofile-config-api::jar:3.0.2 +org.eclipse.microprofile.context-propagation:microprofile-context-propagation-api::jar:1.3 +org.eclipse.microprofile.health:microprofile-health-api::jar:4.0.1 +org.eclipse.microprofile.openapi:microprofile-openapi-api::jar:3.1 +org.eclipse.microprofile.reactive-streams-operators:microprofile-reactive-streams-operators-api::jar:3.0 +org.eclipse.microprofile.rest.client:microprofile-rest-client-api::jar:3.0.1 +org.eclipse.parsson:parsson::jar:1.1.4 +org.eclipse.sisu:org.eclipse.sisu.inject::jar:0.3.5 +org.freemarker:freemarker::jar:2.3.32 +org.fusesource.jansi:jansi::jar:2.4.0 +org.glassfish.expressly:expressly::jar:5.0.0 +org.glassfish.jaxb:jaxb-core::jar:4.0.3 +org.glassfish.jaxb:jaxb-runtime::jar:4.0.3 +org.glassfish.jaxb:txw2::jar:4.0.3 +org.graalvm.sdk:graal-sdk::jar:23.0.1 +org.hdrhistogram:HdrHistogram::jar:2.1.12 +org.hibernate.common:hibernate-commons-annotations::jar:6.0.6.Final +org.hibernate.orm:hibernate-community-dialects::jar:6.2.13.Final +org.hibernate.orm:hibernate-core::jar:6.2.13.Final +org.hibernate.orm:hibernate-graalvm::jar:6.2.13.Final +org.hibernate.validator:hibernate-validator::jar:8.0.1.Final +org.hibernate:quarkus-local-cache::jar:0.2.1 +org.jboss.invocation:jboss-invocation::jar:2.0.0.Final +org.jboss.logging:commons-logging-jboss-logging::jar:1.0.0.Final +org.jboss.logging:jboss-logging-annotations::jar:2.2.1.Final +org.jboss.logging:jboss-logging::jar:3.4.3.Final +org.jboss.logmanager:jboss-logmanager::jar:3.0.2.Final +org.jboss.narayana.jta:narayana-jta::jar:7.0.0.Final +org.jboss.narayana.jts:narayana-jts-integration::jar:7.0.0.Final +org.jboss.resteasy.microprofile:microprofile-config::jar:2.1.4.Final +org.jboss.resteasy.microprofile:microprofile-rest-client-base::jar:2.1.4.Final +org.jboss.resteasy.microprofile:microprofile-rest-client::jar:2.1.4.Final +org.jboss.resteasy:resteasy-client-api::jar:6.2.5.Final +org.jboss.resteasy:resteasy-client::jar:6.2.5.Final +org.jboss.resteasy:resteasy-core-spi::jar:6.2.5.Final +org.jboss.resteasy:resteasy-core::jar:6.2.5.Final +org.jboss.resteasy:resteasy-jackson2-provider::jar:6.2.5.Final +org.jboss.slf4j:slf4j-jboss-logmanager::jar:2.0.0.Final +org.jboss.threads:jboss-threads::jar:3.5.0.Final +org.jboss:jboss-transaction-spi::jar:8.0.0.Final +org.jetbrains:annotations::jar:24.1.0 +org.junit.jupiter:junit-jupiter-api::jar:5.9.3 +org.junit.jupiter:junit-jupiter-engine::jar:5.9.3 +org.junit.jupiter:junit-jupiter-params::jar:5.9.3 +org.junit.jupiter:junit-jupiter::jar:5.9.3 +org.junit.platform:junit-platform-commons::jar:1.9.3 +org.junit.platform:junit-platform-engine::jar:1.9.3 +org.junit.platform:junit-platform-launcher::jar:1.9.3 +org.latencyutils:LatencyUtils::jar:2.0.3 +org.liquibase:liquibase-core::jar:4.20.0 +org.mvnpm.at.lit-labs:ssr-dom-shim::jar:1.1.1 +org.mvnpm.at.lit:reactive-element::jar:1.6.3 +org.mvnpm.at.mvnpm:vaadin-webcomponents::jar:24.1.6 +org.mvnpm.at.open-wc:dedupe-mixin::jar:1.4.0 +org.mvnpm.at.polymer:polymer::jar:3.5.1 +org.mvnpm.at.types:trusted-types::jar:2.0.3 +org.mvnpm.at.vaadin:router::jar:1.7.5 +org.mvnpm.at.vaadin:vaadin-development-mode-detector::jar:2.0.6 +org.mvnpm.at.vaadin:vaadin-usage-statistics::jar:2.1.2 +org.mvnpm.at.vanillawc:wc-codemirror::jar:2.1.0 +org.mvnpm.at.webcomponents:shadycss::jar:1.11.2 +org.mvnpm:echarts::jar:5.4.3 +org.mvnpm:es-module-shims::jar:1.8.0 +org.mvnpm:lit-element-state::jar:1.7.0 +org.mvnpm:lit-element::jar:3.3.3 +org.mvnpm:lit-html::jar:2.8.0 +org.mvnpm:lit::jar:2.8.0 +org.mvnpm:path-to-regexp::jar:2.4.0 +org.mvnpm:tslib::jar:2.3.0 +org.mvnpm:zrender::jar:5.4.4 +org.opentest4j:opentest4j::jar:1.2.0 +org.osgi:osgi.core::jar:6.0.0 +org.ow2.asm:asm-analysis::jar:9.5 +org.ow2.asm:asm-commons::jar:9.5 +org.ow2.asm:asm-tree::jar:9.5 +org.ow2.asm:asm-util::jar:9.5 +org.ow2.asm:asm::jar:9.5 +org.reactivestreams:reactive-streams::jar:1.0.4 +org.slf4j:slf4j-api::jar:2.0.9 +org.springframework:spring-core::jar:6.1.6 +org.springframework:spring-jcl::jar:6.0.13 +org.unbescape:unbescape::jar:1.1.6.RELEASE +org.wildfly.common:wildfly-common::jar:1.5.4.Final-format-001 +org.xerial:sqlite-jdbc::jar:3.41.2.2 +org.yaml:snakeyaml::jar:1.33 diff --git a/api-server/target/quarkus-app/quarkus/quarkus-application.dat b/api-server/target/quarkus-app/quarkus/quarkus-application.dat new file mode 100644 index 0000000000000000000000000000000000000000..2a72b4a9dc1084a146dd457081aa2e5f67f9b95e GIT binary patch literal 180591 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 Kd&Q<5Jo10bCSP#? literal 0 HcmV?d00001 diff --git a/api-server/target/quarkus-artifact.properties b/api-server/target/quarkus-artifact.properties new file mode 100644 index 0000000..5331494 --- /dev/null +++ b/api-server/target/quarkus-artifact.properties @@ -0,0 +1,4 @@ +#Generated by Quarkus - Do not edit manually +#Sun May 12 16:25:48 CEST 2024 +path=quarkus-app/quarkus-run.jar +type=jar diff --git a/cli/.DS_Store b/cli/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4924e665ec2c326533b7e495451ffa046baf9aaa GIT binary patch literal 6148 zcmeHK&2G~`5S~p!>jV{Y0MVl_+@dHh70?4x8p5Fx2apj+6x7;rlvuL8R_u^M2=bi= zKyX0308hcG;)aB{@)(E<%s7)!ohMhdwnFj3<->&C-;PT&C=lbs9<2(Js!sN8h|%nu~GRU?{?ilx^dtqX{+^( zmCBWKE31Z4GiqzKr}mIf?9@reNyi!O($hUB9lFYXz+}N~c4RgA+(Q25R*RIcIRpZK4 z>(^D_hTVW(b=ttTj>h sQAsE+lPFTKQOB_`=qO%C6@osK48*p=Od|H6=syC22Hj`|{wV{$0VN5* + + + + + + + + + \ No newline at end of file diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 0000000..c22453f --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,224 @@ + + 4.0.0 + cli + 1.0-SNAPSHOT + cli + CLI for ResourceTracker + + + com.repoachiever + base + 1.0-SNAPSHOT + + + + + com.repoachiever.CLI + + + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webflux + + + + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 + + + + + io.swagger.core.v3 + swagger-annotations + + + io.swagger.core.v3 + swagger-models + + + org.openapitools + jackson-databind-nullable + + + + + info.picocli + picocli + + + info.picocli + picocli-spring-boot-starter + + + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.projectlombok + lombok + + + org.yaml + snakeyaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.opencsv + opencsv + + + commons-io + commons-io + + + me.tongfei + progressbar + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + + + junit + junit + + + + + cli + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + + + + ${basedir}/target/cli.jar + ${main.basedir}/../bin/cli/cli.jar + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + + + + org.openapitools + openapi-generator-maven-plugin + + + + generate + + + ${project.basedir}/../api-server/src/main/openapi/openapi.yml + java + webclient + ${default.package}.api + ${default.package}.model + true + false + false + false + false + false + true + false + + @lombok.Data @lombok.AllArgsConstructor(staticName = "of") + src/main/java + true + false + true + true + false + true + java8 + true + true + + + + + + + pl.project13.maven + git-commit-id-plugin + + + + + + + dev + + true + + + dev + + + + prod + + prod + + + + diff --git a/cli/src/.DS_Store b/cli/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d1aacd23774ddb7861278c22e6d1a94938f189c4 GIT binary patch literal 6148 zcmeHKJ5Iwu5S=wv)Kwg*BhM9tNxHv@2~A^)FIDxOpF0z zz!=yS1~9W(l6^&6jR9l87^oQF?}LXjrU6Su`E+2BD*!NtSp<8&OK^@4m*PE7%&E!4D9LSkmvu&_WOS` z$)1b>W8hyg;NonOjnPt=t)1q0)_Uj#l!fC;#VQ3G8O4a@QG5s$f!*^4m NfY4xzG4QJld;>`SSRDWW literal 0 HcmV?d00001 diff --git a/cli/src/main/java/com/repoachiever/App.java b/cli/src/main/java/com/repoachiever/App.java new file mode 100644 index 0000000..b762dd6 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/App.java @@ -0,0 +1,94 @@ +package com.repoachiever; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.client.command.*; +import com.repoachiever.service.command.BaseCommandService; +// import com.repoachiever.service.KafkaConsumerWrapper; +import com.repoachiever.service.command.external.start.StartExternalCommandService; +import com.repoachiever.service.command.external.start.provider.aws.AWSStartExternalCommandService; +import com.repoachiever.service.command.external.state.StateExternalCommandService; +import com.repoachiever.service.command.external.state.provider.aws.AWSStateExternalCommandService; +import com.repoachiever.service.command.external.stop.StopExternalCommandService; +import com.repoachiever.service.command.external.stop.provider.aws.AWSStopExternalCommandService; +import com.repoachiever.service.command.external.version.VersionExternalCommandService; +import com.repoachiever.service.command.internal.health.HealthCheckInternalCommandService; +import com.repoachiever.service.command.internal.readiness.ReadinessCheckInternalCommandService; +import com.repoachiever.service.command.internal.readiness.provider.aws.AWSReadinessCheckInternalCommandService; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.config.common.ValidConfigService; +import com.repoachiever.service.visualization.VisualizationService; +import com.repoachiever.service.visualization.common.label.StartCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.StateCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.StopCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.VersionCommandVisualizationLabel; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; +import picocli.CommandLine; + +@Component +@Import({ + BaseCommandService.class, + StartExternalCommandService.class, + AWSStateExternalCommandService.class, + StateExternalCommandService.class, + StopExternalCommandService.class, + VersionExternalCommandService.class, + HealthCheckInternalCommandService.class, + ReadinessCheckInternalCommandService.class, + AWSReadinessCheckInternalCommandService.class, + ApplyClientCommandService.class, + DestroyClientCommandService.class, + HealthCheckClientCommandService.class, + ReadinessCheckClientCommandService.class, + LogsClientCommandService.class, + ScriptAcquireClientCommandService.class, + SecretsAcquireClientCommandService.class, + VersionClientCommandService.class, + AWSStartExternalCommandService.class, + AWSStopExternalCommandService.class, + ConfigService.class, + ValidConfigService.class, + BuildProperties.class, + PropertiesEntity.class, + StartCommandVisualizationLabel.class, + StopCommandVisualizationLabel.class, + StateCommandVisualizationLabel.class, + VersionCommandVisualizationLabel.class, + VisualizationService.class, + VisualizationState.class +}) +public class App implements ApplicationRunner, ExitCodeGenerator { + private static final Logger logger = LogManager.getLogger(App.class); + + private int exitCode; + + @Autowired private ValidConfigService validConfigService; + + @Autowired private BaseCommandService baseCommandService; + + @Override + public void run(ApplicationArguments args) { + try { + validConfigService.validate(); + } catch (Exception e) { + logger.fatal(e.getMessage()); + return; + } + + CommandLine cmd = new CommandLine(baseCommandService); + exitCode = cmd.execute(args.getSourceArgs()); + } + + @Override + public int getExitCode() { + return exitCode; + } +} diff --git a/cli/src/main/java/com/repoachiever/CLI.java b/cli/src/main/java/com/repoachiever/CLI.java new file mode 100644 index 0000000..02ee6ef --- /dev/null +++ b/cli/src/main/java/com/repoachiever/CLI.java @@ -0,0 +1,13 @@ +package com.repoachiever; + +import com.repoachiever.service.client.command.*; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CLI { + public static void main(String[] args) { + SpringApplication application = new SpringApplication(App.class); + System.exit(SpringApplication.exit(application.run(args))); + } +} diff --git a/cli/src/main/java/com/repoachiever/converter/CredentialsConverter.java b/cli/src/main/java/com/repoachiever/converter/CredentialsConverter.java new file mode 100644 index 0000000..e6abb88 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/converter/CredentialsConverter.java @@ -0,0 +1,11 @@ +package com.repoachiever.converter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class CredentialsConverter { + @SuppressWarnings("unchecked") + public static T convert(Object input, Class stub) { + ObjectMapper mapper = new ObjectMapper(); + return mapper.convertValue(input, stub); + } +} diff --git a/cli/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java b/cli/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java new file mode 100644 index 0000000..1749e85 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java @@ -0,0 +1,12 @@ +package com.repoachiever.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents script validation application used for script acquiring process. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ValidationScriptApplicationDto { + private List fileContent; +} diff --git a/cli/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java b/cli/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java new file mode 100644 index 0000000..288b23c --- /dev/null +++ b/cli/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java @@ -0,0 +1,14 @@ +package com.repoachiever.dto; + +import com.repoachiever.model.Provider; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents secrets validation application used for secrets acquiring process. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ValidationSecretsApplicationDto { + private Provider provider; + + private String filePath; +} diff --git a/cli/src/main/java/com/repoachiever/dto/VisualizationLabelDto.java b/cli/src/main/java/com/repoachiever/dto/VisualizationLabelDto.java new file mode 100644 index 0000000..b971900 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/dto/VisualizationLabelDto.java @@ -0,0 +1,26 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; + +/** */ +@AllArgsConstructor(staticName = "of") +public class VisualizationLabelDto { + private final String message; + + private final Integer percentage; + + @Override + public String toString() { + int filledLength = (int) Math.round((double) percentage / 100 * 20); + int emptyLength = 20 - filledLength; + + return "[" + + "#".repeat(Math.max(0, filledLength)) + + "-".repeat(Math.max(0, emptyLength)) + + "] " + + percentage + + "%" + + " " + + message; + } +} diff --git a/cli/src/main/java/com/repoachiever/entity/ConfigEntity.java b/cli/src/main/java/com/repoachiever/entity/ConfigEntity.java new file mode 100644 index 0000000..ee87ff5 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/entity/ConfigEntity.java @@ -0,0 +1,72 @@ +package com.repoachiever.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** Represents configuration model used for ResourceTracker deployment operation. */ +@Getter +public class ConfigEntity { + /** Represents request to be executed in remote environment. */ + @Getter + public static class Request { + @NotBlank public String name; + + @Pattern(regexp = "^(((./)?)|((~/.)?)|((/?))?)([a-zA-Z/]*)((\\.([a-z]+))?)$") + public String file; + + @Pattern( + regexp = + "(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\\?])|([\\*]))[\\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\\?])|([\\*]))[\\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\\?])|([\\*]))[\\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\\?])|([\\*]))([\\s]?(([\\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?|" + + " (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?|" + + " ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)") + public String frequency; + } + + @Valid @NotNull public List requests; + + /** + * Represents remove cloud infrastructure configuration properties used for further deployment + * related operations. + */ + @Getter + public static class Cloud { + @JsonFormat(shape = JsonFormat.Shape.OBJECT) + public enum Provider { + @JsonProperty("aws") + AWS, + } + + @NotNull public Provider provider; + + @Getter + @NoArgsConstructor + public static class AWSCredentials { + @Pattern(regexp = "^(((./)?)|((~/.)?)|((/?))?)([a-zA-Z/]*)((\\.([a-z]+))?)$") + public String file; + + @NotBlank public String region; + } + + @NotNull public Object credentials; + } + + @Valid @NotNull public Cloud cloud; + + /** Represents API Server configuration used for further connection establishment. */ + @Getter + public static class APIServer { + @NotBlank public String host; + } + + @Valid + @NotNull + @JsonProperty("api-server") + public APIServer apiServer; +} diff --git a/cli/src/main/java/com/repoachiever/entity/PropertiesEntity.java b/cli/src/main/java/com/repoachiever/entity/PropertiesEntity.java new file mode 100644 index 0000000..772f766 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/entity/PropertiesEntity.java @@ -0,0 +1,79 @@ +package com.repoachiever.entity; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.io.ClassPathResource; + +/** Represents application properties used for application configuration. */ +@Getter +@Configuration +public class PropertiesEntity { + private static final String GIT_CONFIG_PROPERTIES_FILE = "git.properties"; + + @Value(value = "${git.commit.id.abbrev}") + private String gitCommitId; + + @Value(value = "${config.root}") + private String configRootPath; + + @Value(value = "${config.user.file}") + private String configUserFilePath; + + @Value(value = "${progress.visualization.period}") + private Integer progressVisualizationPeriod; + + @Value(value = "${progress.visualization.secrets-acquire-request}") + private String progressVisualizationSecretsAcquireRequestLabel; + + @Value(value = "${progress.visualization.script-acquire-request}") + private String progressVisualizationScriptAcquireRequestLabel; + + @Value(value = "${progress.visualization.apply-request}") + private String progressVisualizationApplyRequestLabel; + + @Value(value = "${progress.visualization.apply-response}") + private String progressVisualizationApplyResponseLabel; + + @Value(value = "${progress.visualization.destroy-request}") + private String progressVisualizationDestroyRequestLabel; + + @Value(value = "${progress.visualization.destroy-response}") + private String progressVisualizationDestroyResponseLabel; + + @Value(value = "${progress.visualization.state-request}") + private String progressVisualizationStateRequestLabel; + + @Value(value = "${progress.visualization.state-response}") + private String progressVisualizationStateResponseLabel; + + @Value(value = "${progress.visualization.version-info-request}") + private String progressVisualizationVersionInfoRequestLabel; + + @Value(value = "${progress.visualization.health-check-request}") + private String progressVisualizationHealthCheckRequestLabel; + + @Value(value = "${progress.visualization.readiness-check-request}") + private String progressVisualizationReadinessCheckRequestLabel; + + @Bean + private static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { + PropertySourcesPlaceholderConfigurer propsConfig = new PropertySourcesPlaceholderConfigurer(); + propsConfig.setLocation(new ClassPathResource(GIT_CONFIG_PROPERTIES_FILE)); + propsConfig.setIgnoreResourceNotFound(true); + propsConfig.setIgnoreUnresolvablePlaceholders(true); + return propsConfig; + } + + /** + * Removes the last symbol in git commit id of the repository. + * + * @return chopped repository git commit id. + */ + public String getGitCommitId() { + return StringUtils.chop(gitCommitId); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/ApiServerException.java b/cli/src/main/java/com/repoachiever/exception/ApiServerException.java new file mode 100644 index 0000000..d89b655 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/ApiServerException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ApiServerException extends IOException { + public ApiServerException() { + this(""); + } + + public ApiServerException(Object... message) { + super( + new Formatter() + .format("API Server exception: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java b/cli/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java new file mode 100644 index 0000000..471bacb --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ApiServerNotAvailableException extends IOException { + public ApiServerNotAvailableException() { + this(""); + } + + public ApiServerNotAvailableException(Object... message) { + super( + new Formatter() + .format("API Server is not available: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java b/cli/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java new file mode 100644 index 0000000..43961c6 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class CloudCredentialsFileNotFoundException extends IOException { + public CloudCredentialsFileNotFoundException() { + this(""); + } + + public CloudCredentialsFileNotFoundException(Object... message) { + super( + new Formatter() + .format( + "Given cloud credentials file is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java b/cli/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java new file mode 100644 index 0000000..713b34f --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class CloudCredentialsValidationException extends IOException { + public CloudCredentialsValidationException() { + this(""); + } + + public CloudCredentialsValidationException(Object... message) { + super( + new Formatter() + .format("Given cloud credentials are not valid!: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/ConfigValidationException.java b/cli/src/main/java/com/repoachiever/exception/ConfigValidationException.java new file mode 100644 index 0000000..9b78871 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/ConfigValidationException.java @@ -0,0 +1,14 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ConfigValidationException extends IOException { + public ConfigValidationException(Object... message) { + super( + new Formatter() + .format("Config file content is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/ScriptDataFileNotFoundException.java b/cli/src/main/java/com/repoachiever/exception/ScriptDataFileNotFoundException.java new file mode 100644 index 0000000..753feee --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/ScriptDataFileNotFoundException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ScriptDataFileNotFoundException extends IOException { + public ScriptDataFileNotFoundException() { + this(""); + } + + public ScriptDataFileNotFoundException(Object... message) { + super( + new Formatter() + .format("Given explicit script file is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java b/cli/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java new file mode 100644 index 0000000..17250fb --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** Represents exception, when given file is not valid. */ +public class ScriptDataValidationException extends IOException { + public ScriptDataValidationException() { + this(""); + } + + public ScriptDataValidationException(Object... message) { + super( + new Formatter() + .format("Given explicit script file is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/exception/VersionMismatchException.java b/cli/src/main/java/com/repoachiever/exception/VersionMismatchException.java new file mode 100644 index 0000000..b8ffe86 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/exception/VersionMismatchException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** Represents exception, when API Server version is different from the version of the client. */ +public class VersionMismatchException extends IOException { + public VersionMismatchException() { + this(""); + } + + public VersionMismatchException(Object... message) { + super( + new Formatter() + .format( + "API Server version is different from the version of the client: %s", + Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cli/src/main/java/com/repoachiever/logging/FatalAppender.java b/cli/src/main/java/com/repoachiever/logging/FatalAppender.java new file mode 100644 index 0000000..53ff3e2 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/logging/FatalAppender.java @@ -0,0 +1,48 @@ +package com.repoachiever.logging; + +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +@Plugin(name = "FatalAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class FatalAppender extends AbstractAppender { + @Autowired private ApplicationContext context; + + private ConcurrentMap eventMap = new ConcurrentHashMap<>(); + + @SuppressWarnings("deprecation") + protected FatalAppender(String name, Filter filter) { + super(name, filter, null); + } + + @PluginFactory + public static FatalAppender createAppender( + @PluginAttribute("name") String name, @PluginElement("Filter") Filter filter) { + return new FatalAppender(name, filter); + } + + @Override + public void append(LogEvent event) { + if (event.getLevel().equals(Level.FATAL)) { + SpringApplication.exit(context, () -> 1); + System.exit(1); + } + + eventMap.put(Instant.now().toString(), event); + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/IClientCommand.java b/cli/src/main/java/com/repoachiever/service/client/IClientCommand.java new file mode 100644 index 0000000..a81043d --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/IClientCommand.java @@ -0,0 +1,19 @@ +package com.repoachiever.service.client; + +import com.repoachiever.exception.ApiServerException; + +/** + * Represents external resource command interface. + * + * @param type of the command response. + * @param type of the command request. + */ +public interface IClientCommand { + /** + * Processes certain request for an external command. + * + * @param input input to be given as request body. + * @return command response. + */ + T process(K input) throws ApiServerException; +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java new file mode 100644 index 0000000..02db9c4 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java @@ -0,0 +1,44 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TerraformResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TerraformDeploymentApplication; +import com.repoachiever.model.TerraformDeploymentApplicationResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents apply client command service. */ +@Service +public class ApplyClientCommandService + implements IClientCommand< + TerraformDeploymentApplicationResult, TerraformDeploymentApplication> { + private final TerraformResourceApi terraformResourceApi; + + public ApplyClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.terraformResourceApi = new TerraformResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public TerraformDeploymentApplicationResult process(TerraformDeploymentApplication input) + throws ApiServerException { + try { + return terraformResourceApi.v1TerraformApplyPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java new file mode 100644 index 0000000..4a1d297 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java @@ -0,0 +1,40 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TerraformResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TerraformDestructionApplication; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents destroy client command service. */ +@Service +public class DestroyClientCommandService + implements IClientCommand { + private final TerraformResourceApi terraformResourceApi; + + public DestroyClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.terraformResourceApi = new TerraformResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public Void process(TerraformDestructionApplication input) throws ApiServerException { + try { + return terraformResourceApi.v1TerraformDestroyPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java new file mode 100644 index 0000000..99ec67e --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java @@ -0,0 +1,39 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.HealthResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents health check client command service. */ +@Service +public class HealthCheckClientCommandService implements IClientCommand { + private final HealthResourceApi healthResourceApi; + + public HealthCheckClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.healthResourceApi = new HealthResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public HealthCheckResult process(Void input) throws ApiServerException { + try { + return healthResourceApi.v1HealthGet().block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java new file mode 100644 index 0000000..57dd5e8 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java @@ -0,0 +1,42 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TopicResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TopicLogsApplication; +import com.repoachiever.model.TopicLogsResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents logs topic client command service. */ +@Service +public class LogsClientCommandService + implements IClientCommand { + private final TopicResourceApi topicResourceApi; + + public LogsClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.topicResourceApi = new TopicResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public TopicLogsResult process(TopicLogsApplication input) throws ApiServerException { + try { + return topicResourceApi.v1TopicLogsPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java new file mode 100644 index 0000000..4ab1ea2 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java @@ -0,0 +1,41 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.HealthResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ReadinessCheckApplication; +import com.repoachiever.model.ReadinessCheckResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents readiness check client command service. */ +@Service +public class ReadinessCheckClientCommandService + implements IClientCommand { + private final HealthResourceApi healthResourceApi; + + public ReadinessCheckClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.healthResourceApi = new HealthResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public ReadinessCheckResult process(ReadinessCheckApplication input) throws ApiServerException { + try { + return healthResourceApi.v1ReadinessPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java new file mode 100644 index 0000000..2907b28 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java @@ -0,0 +1,46 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.ValidationResourceApi; +import com.repoachiever.dto.ValidationScriptApplicationDto; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ValidationScriptApplication; +import com.repoachiever.model.ValidationScriptApplicationResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents script validation client command service. */ +@Service +public class ScriptAcquireClientCommandService + implements IClientCommand { + private final ValidationResourceApi validationResourceApi; + + public ScriptAcquireClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.validationResourceApi = new ValidationResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public ValidationScriptApplicationResult process(ValidationScriptApplicationDto input) + throws ApiServerException { + try { + return validationResourceApi + .v1ScriptAcquirePost(ValidationScriptApplication.of(input.getFileContent())) + .block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java new file mode 100644 index 0000000..cc93841 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java @@ -0,0 +1,66 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.ValidationResourceApi; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.exception.CloudCredentialsFileNotFoundException; +import com.repoachiever.model.Provider; +import com.repoachiever.model.ValidationSecretsApplication; +import com.repoachiever.model.ValidationSecretsApplicationResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents secrets validation client command service. */ +@Service +public class SecretsAcquireClientCommandService + implements IClientCommand { + private final ValidationResourceApi validationResourceApi; + + public SecretsAcquireClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.validationResourceApi = new ValidationResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public ValidationSecretsApplicationResult process(ValidationSecretsApplicationDto input) + throws ApiServerException { + Path filePath = Paths.get(input.getFilePath()); + + if (Files.notExists(filePath)) { + throw new ApiServerException(new CloudCredentialsFileNotFoundException().getMessage()); + } + + String content; + + try { + content = Files.readString(filePath); + } catch (IOException e) { + throw new ApiServerException(e.getMessage()); + } + + try { + return validationResourceApi + .v1SecretsAcquirePost(ValidationSecretsApplication.of(Provider.AWS, content)) + .block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getHeaders()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java b/cli/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java new file mode 100644 index 0000000..e80dec6 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java @@ -0,0 +1,39 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.InfoResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ApplicationInfoResult; +import com.repoachiever.service.client.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents version information client command service. */ +@Service +public class VersionClientCommandService implements IClientCommand { + private final InfoResourceApi infoResourceApi; + + public VersionClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.infoResourceApi = new InfoResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public ApplicationInfoResult process(Void input) throws ApiServerException { + try { + return infoResourceApi.v1InfoVersionGet().block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/BaseCommandService.java b/cli/src/main/java/com/repoachiever/service/command/BaseCommandService.java new file mode 100644 index 0000000..85dcc37 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/BaseCommandService.java @@ -0,0 +1,134 @@ +package com.repoachiever.service.command; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.service.command.external.start.StartExternalCommandService; +import com.repoachiever.service.command.external.state.StateExternalCommandService; +import com.repoachiever.service.command.external.stop.StopExternalCommandService; +import com.repoachiever.service.command.external.version.VersionExternalCommandService; +import com.repoachiever.service.command.internal.health.HealthCheckInternalCommandService; +import com.repoachiever.service.command.internal.readiness.ReadinessCheckInternalCommandService; +import com.repoachiever.service.visualization.VisualizationService; +import com.repoachiever.service.visualization.common.label.StartCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.StateCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.StopCommandVisualizationLabel; +import com.repoachiever.service.visualization.common.label.VersionCommandVisualizationLabel; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import picocli.CommandLine.Command; + +/** Represents general command management service. */ +@Service +@Command( + name = "help", + mixinStandardHelpOptions = true, + description = "Cloud-based remote resource tracker", + version = "1.0") +public class BaseCommandService { + private static final Logger logger = LogManager.getLogger(BaseCommandService.class); + + @Autowired private StartExternalCommandService startCommandService; + + @Autowired private StateExternalCommandService stateCommandService; + + @Autowired private StopExternalCommandService stopCommandService; + + @Autowired private VersionExternalCommandService versionCommandService; + + @Autowired private HealthCheckInternalCommandService healthCheckInternalCommandService; + + @Autowired private ReadinessCheckInternalCommandService readinessCheckInternalCommandService; + + @Autowired private StartCommandVisualizationLabel startCommandVisualizationLabel; + + @Autowired private StopCommandVisualizationLabel stopCommandVisualizationLabel; + + @Autowired private StateCommandVisualizationLabel stateCommandVisualizationLabel; + + @Autowired private VersionCommandVisualizationLabel versionCommandVisualizationLabel; + + @Autowired private VisualizationService visualizationService; + + @Autowired private VisualizationState visualizationState; + + /** Provides access to start command service. */ + @Command(description = "Start remote requests execution") + private void start() { + visualizationState.setLabel(startCommandVisualizationLabel); + + visualizationService.process(); + + try { + healthCheckInternalCommandService.process(); + + startCommandService.process(); + } catch (ApiServerException e) { + logger.fatal(e.getMessage()); + return; + } + + visualizationService.await(); + } + + /** Provides access to state command service. */ + @Command(description = "Retrieve state of remote requests executions") + private void state() { + visualizationState.setLabel(stateCommandVisualizationLabel); + + visualizationService.process(); + + try { + healthCheckInternalCommandService.process(); + readinessCheckInternalCommandService.process(); + + stateCommandService.process(); + } catch (ApiServerException e) { + logger.fatal(e.getMessage()); + return; + } + + visualizationService.await(); + } + + /** Provides access to stop command service. */ + @Command(description = "Stop remote requests execution") + private void stop() { + visualizationState.setLabel(stopCommandVisualizationLabel); + + visualizationService.process(); + + try { + healthCheckInternalCommandService.process(); + + stopCommandService.process(); + } catch (ApiServerException e) { + logger.fatal(e.getMessage()); + return; + } + + visualizationService.await(); + } + + /** Provides access to version command service. */ + @Command( + description = + "Retrieve version of ResourceTracker CLI and ResourceTracker API Server(if available)") + private void version() { + visualizationState.setLabel(versionCommandVisualizationLabel); + + visualizationService.process(); + + try { + healthCheckInternalCommandService.process(); + + versionCommandService.process(); + } catch (ApiServerException e) { + logger.fatal(e.getMessage()); + return; + } + + visualizationService.await(); + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/common/ICommand.java b/cli/src/main/java/com/repoachiever/service/command/common/ICommand.java new file mode 100644 index 0000000..0fc0e70 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/common/ICommand.java @@ -0,0 +1,9 @@ +package com.repoachiever.service.command.common; + +import com.repoachiever.exception.ApiServerException; + +/** Represents common command interface. */ +public interface ICommand { + /** Processes certain request for an external command. */ + void process() throws ApiServerException; +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java new file mode 100644 index 0000000..7e64cea --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java @@ -0,0 +1,26 @@ +package com.repoachiever.service.command.external.start; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.*; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.start.provider.aws.AWSStartExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service. */ +@Service +public class StartExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStartExternalCommandService awsStartExternalCommandService; + + /** + * @see ICommand + */ + public void process() throws ApiServerException { + switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsStartExternalCommandService.process(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java new file mode 100644 index 0000000..2926fdc --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java @@ -0,0 +1,120 @@ +package com.repoachiever.service.command.external.start.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.ValidationScriptApplicationDto; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.exception.ScriptDataValidationException; +import com.repoachiever.exception.VersionMismatchException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.ApplyClientCommandService; +import com.repoachiever.service.client.command.ScriptAcquireClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.client.command.VersionClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.visualization.state.VisualizationState; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service for AWS provider. */ +@Service +public class AWSStartExternalCommandService implements ICommand { + private static final Logger logger = LogManager.getLogger(AWSStartExternalCommandService.class); + + @Autowired private ConfigService configService; + + @Autowired private PropertiesEntity properties; + + @Autowired private ApplyClientCommandService applyClientCommandService; + + @Autowired private VersionClientCommandService versionClientCommandService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private ScriptAcquireClientCommandService scriptAcquireClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + + if (validationSecretsApplicationResult.getValid()) { + visualizationState.getLabel().pushNext(); + + ApplicationInfoResult applicationInfoResult = versionClientCommandService.process(null); + + if (!applicationInfoResult.getExternalApi().getHash().equals(properties.getGitCommitId())) { + throw new ApiServerException(new VersionMismatchException().getMessage()); + } + + List requests = + configService.getConfig().getRequests().stream() + .map( + element -> { + try { + return DeploymentRequest.of( + element.getName(), + Files.readString(Paths.get(element.getFile())), + element.getFrequency()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .toList(); + + ValidationScriptApplicationDto validationScriptApplicationDto = + ValidationScriptApplicationDto.of( + requests.stream().map(DeploymentRequest::getScript).toList()); + + ValidationScriptApplicationResult validationScriptApplicationResult = + scriptAcquireClientCommandService.process(validationScriptApplicationDto); + + if (validationScriptApplicationResult.getValid()) { + visualizationState.getLabel().pushNext(); + + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TerraformDeploymentApplication terraformDeploymentApplication = + TerraformDeploymentApplication.of(requests, Provider.AWS, credentialsFields); + + applyClientCommandService.process(terraformDeploymentApplication); + + visualizationState.getLabel().pushNext(); + } else { + logger.fatal(new ScriptDataValidationException().getMessage()); + } + } else { + logger.fatal(new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java new file mode 100644 index 0000000..233e319 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java @@ -0,0 +1,25 @@ +package com.repoachiever.service.command.external.state; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.state.provider.aws.AWSStateExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents state external command service. */ +@Service +public class StateExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStateExternalCommandService awsStateExternalCommandService; + + /** + * @see ICommand + */ + public void process() throws ApiServerException { + switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsStateExternalCommandService.process(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java new file mode 100644 index 0000000..f01cc5f --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java @@ -0,0 +1,86 @@ +package com.repoachiever.service.command.external.state.provider.aws; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.LogsClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service for AWS provider. */ +@Service +public class AWSStateExternalCommandService implements ICommand { + private static final Logger logger = LogManager.getLogger(AWSStateExternalCommandService.class); + + @Autowired private ConfigService configService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private LogsClientCommandService logsClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + + if (validationSecretsApplicationResult.getValid()) { + visualizationState.getLabel().pushNext(); + + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TopicLogsResult topicLogsResult; + + TopicLogsApplication topicLogsApplication = + TopicLogsApplication.of(Provider.AWS, credentialsFields); + + topicLogsResult = logsClientCommandService.process(topicLogsApplication); + + visualizationState.getLabel().pushNext(); + + ObjectMapper mapper = new ObjectMapper(); + topicLogsResult + .getResult() + .forEach( + element -> { + try { + visualizationState.addResult(mapper.writeValueAsString(element)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + } else { + logger.fatal(new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java new file mode 100644 index 0000000..1a393c7 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java @@ -0,0 +1,26 @@ +package com.repoachiever.service.command.external.stop; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.*; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.stop.provider.aws.AWSStopExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents stop external command service. */ +@Service +public class StopExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStopExternalCommandService stopExternalCommandService; + + /** + * @see ICommand + */ + public void process() throws ApiServerException { + switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> stopExternalCommandService.process(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java new file mode 100644 index 0000000..7677ed5 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java @@ -0,0 +1,80 @@ +package com.repoachiever.service.command.external.stop.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.AWSSecrets; +import com.repoachiever.model.CredentialsFields; +import com.repoachiever.model.DestructionRequest; +import com.repoachiever.model.Provider; +import com.repoachiever.model.TerraformDestructionApplication; +import com.repoachiever.model.ValidationSecretsApplicationResult; +import com.repoachiever.service.client.command.DestroyClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** */ +@Service +public class AWSStopExternalCommandService implements ICommand { + private static final Logger logger = LogManager.getLogger(AWSStopExternalCommandService.class); + + @Autowired private ConfigService configService; + + @Autowired private DestroyClientCommandService destroyClientCommandService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + + if (validationSecretsApplicationResult.getValid()) { + visualizationState.getLabel().pushNext(); + + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TerraformDestructionApplication terraformDestructionApplication = + TerraformDestructionApplication.of( + configService.getConfig().getRequests().stream() + .map(element -> DestructionRequest.of(element.getName())) + .toList(), + Provider.AWS, + credentialsFields); + + destroyClientCommandService.process(terraformDestructionApplication); + + visualizationState.getLabel().pushNext(); + } else { + logger.fatal(new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java new file mode 100644 index 0000000..54c7bb6 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java @@ -0,0 +1,41 @@ +package com.repoachiever.service.command.external.version; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.ApplicationInfoResult; +import com.repoachiever.service.client.command.HealthCheckClientCommandService; +import com.repoachiever.service.client.command.VersionClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents version external command service. */ +@Service +public class VersionExternalCommandService implements ICommand { + @Autowired PropertiesEntity properties; + + @Autowired private VersionClientCommandService versionClientCommandService; + + @Autowired private HealthCheckClientCommandService healthCheckClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + try { + ApplicationInfoResult applicationInfoResult = versionClientCommandService.process(null); + + visualizationState.addResult( + String.format( + "API Server version: %s", applicationInfoResult.getExternalApi().getHash())); + } finally { + visualizationState.addResult( + String.format("Client version: %s", properties.getGitCommitId())); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java new file mode 100644 index 0000000..90b6b2d --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java @@ -0,0 +1,34 @@ +package com.repoachiever.service.command.internal.health; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.model.HealthCheckStatus; +import com.repoachiever.service.client.command.HealthCheckClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class HealthCheckInternalCommandService implements ICommand { + @Autowired private HealthCheckClientCommandService healthCheckClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + HealthCheckResult healthCheckResult = healthCheckClientCommandService.process(null); + + if (healthCheckResult.getStatus() == HealthCheckStatus.DOWN) { + throw new ApiServerException( + new ApiServerNotAvailableException(healthCheckResult.getChecks().toString()) + .getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java new file mode 100644 index 0000000..b164049 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.command.internal.readiness; + +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.*; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.internal.readiness.provider.aws.AWSReadinessCheckInternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents readiness check internal command service. */ +@Service +public class ReadinessCheckInternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired + private AWSReadinessCheckInternalCommandService awsReadinessCheckInternalCommandService; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsReadinessCheckInternalCommandService.process(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java b/cli/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java new file mode 100644 index 0000000..114cb86 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java @@ -0,0 +1,74 @@ +package com.repoachiever.service.command.internal.readiness.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.ReadinessCheckClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.visualization.state.VisualizationState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AWSReadinessCheckInternalCommandService implements ICommand { + private static final Logger logger = + LogManager.getLogger(AWSReadinessCheckInternalCommandService.class); + + @Autowired private ConfigService configService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private ReadinessCheckClientCommandService readinessCheckClientCommandService; + + @Autowired private VisualizationState visualizationState; + + /** + * @see ICommand + */ + @Override + public void process() throws ApiServerException { + visualizationState.getLabel().pushNext(); + + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + + if (validationSecretsApplicationResult.getValid()) { + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + ReadinessCheckApplication readinessCheckApplication = + ReadinessCheckApplication.of(Provider.AWS, credentialsFields); + + ReadinessCheckResult readinessCheckResult = + readinessCheckClientCommandService.process(readinessCheckApplication); + + if (readinessCheckResult.getStatus() == ReadinessCheckStatus.DOWN) { + throw new ApiServerException( + new ApiServerNotAvailableException(readinessCheckResult.getData().toString()) + .getMessage()); + } + } else { + logger.fatal(new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/config/ConfigService.java b/cli/src/main/java/com/repoachiever/service/config/ConfigService.java new file mode 100644 index 0000000..941d806 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/config/ConfigService.java @@ -0,0 +1,89 @@ +package com.repoachiever.service.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.entity.PropertiesEntity; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +import java.io.*; +import java.nio.file.Paths; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Service for processing configuration file. + */ +@Service +public class ConfigService { + private static final Logger logger = LogManager.getLogger(ConfigService.class); + + private InputStream configFile; + + private ConfigEntity parsedConfigFile; + + /** + * Default constructor, which opens configuration file at the given path. + * + * @param properties common application properties + */ + public ConfigService(@Autowired PropertiesEntity properties) { + try { + configFile = + new FileInputStream( + Paths.get( + System.getProperty("user.home"), + properties.getConfigRootPath(), + properties.getConfigUserFilePath()) + .toString()); + } catch (FileNotFoundException e) { + logger.fatal(e.getMessage()); + } + } + + /** + * Reads configuration from the opened configuration file using mapping with a configuration + * entity. + */ + @PostConstruct + private void configure() { + ObjectMapper mapper = + new ObjectMapper(new YAMLFactory()) + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectReader reader = mapper.reader().forType(new TypeReference() { + }); + + try { + parsedConfigFile = reader.readValues(configFile).readAll().getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } + + /** + * @return Parsed configuration entity + */ + public ConfigEntity getConfig() { + return parsedConfigFile; + } + + @PreDestroy + private void close() { + try { + configFile.close(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/config/common/ValidConfigService.java b/cli/src/main/java/com/repoachiever/service/config/common/ValidConfigService.java new file mode 100644 index 0000000..0f146a2 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/config/common/ValidConfigService.java @@ -0,0 +1,40 @@ +package com.repoachiever.service.config.common; + +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ConfigValidationException; +import com.repoachiever.service.config.ConfigService; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents service for config validation. */ +@Service +public class ValidConfigService { + @Autowired private ConfigService configService; + + /** + * Validates parsed local configuration file. + * + * @throws ConfigValidationException if the configuration validation is not passed. + */ + public void validate() throws ConfigValidationException { + try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { + Validator validator = validatorFactory.getValidator(); + + Set> validationResult = + validator.validate(configService.getConfig()); + + if (!validationResult.isEmpty()) { + throw new ConfigValidationException( + validationResult.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", "))); + } + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/VisualizationService.java b/cli/src/main/java/com/repoachiever/service/visualization/VisualizationService.java new file mode 100644 index 0000000..242e3c5 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/VisualizationService.java @@ -0,0 +1,50 @@ +package com.repoachiever.service.visualization; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.visualization.state.VisualizationState; +import java.util.concurrent.*; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents visualization service used to indicate current execution steps. */ +@Service +public class VisualizationService { + @Autowired private PropertiesEntity properties; + + @Autowired private VisualizationState visualizationState; + + private final ScheduledExecutorService scheduledExecutorService = + Executors.newScheduledThreadPool(2); + + private final CountDownLatch latch = new CountDownLatch(1); + + /** Starts progress visualization processor. */ + public void process() { + scheduledExecutorService.scheduleAtFixedRate( + () -> { + if (visualizationState.getLabel().isNext()) { + System.out.println(visualizationState.getLabel().getCurrent()); + } + + if (visualizationState.getLabel().isEmpty() && !visualizationState.getLabel().isNext()) { + latch.countDown(); + } + }, + 0, + properties.getProgressVisualizationPeriod(), + TimeUnit.MILLISECONDS); + } + + /** Awaits for visualization service to end its processes. */ + @SneakyThrows + public void await() { + latch.await(); + + if (!visualizationState.getResult().isEmpty()) { + System.out.print("\n"); + } + + visualizationState.getResult().forEach(System.out::println); + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/common/IVisualizationLabel.java b/cli/src/main/java/com/repoachiever/service/visualization/common/IVisualizationLabel.java new file mode 100644 index 0000000..3b56108 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/common/IVisualizationLabel.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.visualization.common; + +/** Represents iterative interface for visualization label. */ +public interface IVisualizationLabel { + /** + * Checks if there are steps available in the storage. + * + * @return result of the check. + */ + boolean isEmpty(); + + /** + * Checks if there is next step in the specified label. + * + * @return result of the check. + */ + boolean isNext(); + + /** Pushes next step in the specified label. */ + void pushNext(); + + /** + * Returns string interpretation of the current step. + * + * @return string interpretation of the current step. + */ + String getCurrent(); +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/common/label/StartCommandVisualizationLabel.java b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StartCommandVisualizationLabel.java new file mode 100644 index 0000000..dd0ed06 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StartCommandVisualizationLabel.java @@ -0,0 +1,82 @@ +package com.repoachiever.service.visualization.common.label; + +import com.repoachiever.dto.VisualizationLabelDto; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.visualization.common.IVisualizationLabel; +import java.util.ArrayDeque; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents label set used for apply command service. */ +@Service +public class StartCommandVisualizationLabel implements IVisualizationLabel { + private final ArrayDeque stepsQueue = new ArrayDeque<>(); + + private final ArrayDeque batchQueue = new ArrayDeque<>(); + + private final ReentrantLock mutex = new ReentrantLock(); + + public StartCommandVisualizationLabel(@Autowired PropertiesEntity properties) { + stepsQueue.addAll( + List.of( + VisualizationLabelDto.of( + properties.getProgressVisualizationHealthCheckRequestLabel(), 10), + VisualizationLabelDto.of( + properties.getProgressVisualizationSecretsAcquireRequestLabel(), 30), + VisualizationLabelDto.of( + properties.getProgressVisualizationScriptAcquireRequestLabel(), 60), + VisualizationLabelDto.of(properties.getProgressVisualizationApplyRequestLabel(), 90), + VisualizationLabelDto.of( + properties.getProgressVisualizationApplyResponseLabel(), 100))); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isEmpty() { + return stepsQueue.isEmpty(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isNext() { + mutex.lock(); + + try { + return !batchQueue.isEmpty(); + } finally { + mutex.unlock(); + } + } + + /** + * @see IVisualizationLabel + */ + @Override + public void pushNext() { + mutex.lock(); + + batchQueue.push(stepsQueue.pop().toString()); + + mutex.unlock(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public String getCurrent() { + mutex.lock(); + + try { + return batchQueue.pollLast(); + } finally { + mutex.unlock(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/common/label/StateCommandVisualizationLabel.java b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StateCommandVisualizationLabel.java new file mode 100644 index 0000000..fff547a --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StateCommandVisualizationLabel.java @@ -0,0 +1,82 @@ +package com.repoachiever.service.visualization.common.label; + +import com.repoachiever.dto.VisualizationLabelDto; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.visualization.common.IVisualizationLabel; +import java.util.ArrayDeque; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents label set used for apply command service. */ +@Service +public class StateCommandVisualizationLabel implements IVisualizationLabel { + private final ArrayDeque stepsQueue = new ArrayDeque<>(); + + private final ArrayDeque batchQueue = new ArrayDeque<>(); + + private final ReentrantLock mutex = new ReentrantLock(); + + public StateCommandVisualizationLabel(@Autowired PropertiesEntity properties) { + stepsQueue.addAll( + List.of( + VisualizationLabelDto.of( + properties.getProgressVisualizationHealthCheckRequestLabel(), 10), + VisualizationLabelDto.of( + properties.getProgressVisualizationReadinessCheckRequestLabel(), 30), + VisualizationLabelDto.of( + properties.getProgressVisualizationSecretsAcquireRequestLabel(), 50), + VisualizationLabelDto.of(properties.getProgressVisualizationStateRequestLabel(), 90), + VisualizationLabelDto.of( + properties.getProgressVisualizationStateResponseLabel(), 100))); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isEmpty() { + return stepsQueue.isEmpty(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isNext() { + mutex.lock(); + + try { + return !batchQueue.isEmpty(); + } finally { + mutex.unlock(); + } + } + + /** + * @see IVisualizationLabel + */ + @Override + public void pushNext() { + mutex.lock(); + + batchQueue.push(stepsQueue.pop().toString()); + + mutex.unlock(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public String getCurrent() { + mutex.lock(); + + try { + return batchQueue.pollLast(); + } finally { + mutex.unlock(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/common/label/StopCommandVisualizationLabel.java b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StopCommandVisualizationLabel.java new file mode 100644 index 0000000..136b59d --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/common/label/StopCommandVisualizationLabel.java @@ -0,0 +1,80 @@ +package com.repoachiever.service.visualization.common.label; + +import com.repoachiever.dto.VisualizationLabelDto; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.visualization.common.IVisualizationLabel; +import java.util.ArrayDeque; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents label set used for destroy command service. */ +@Service +public class StopCommandVisualizationLabel implements IVisualizationLabel { + private final ArrayDeque stepsQueue = new ArrayDeque<>(); + + private final ArrayDeque batchQueue = new ArrayDeque<>(); + + private final ReentrantLock mutex = new ReentrantLock(); + + public StopCommandVisualizationLabel(@Autowired PropertiesEntity properties) { + stepsQueue.addAll( + List.of( + VisualizationLabelDto.of( + properties.getProgressVisualizationHealthCheckRequestLabel(), 10), + VisualizationLabelDto.of( + properties.getProgressVisualizationSecretsAcquireRequestLabel(), 50), + VisualizationLabelDto.of(properties.getProgressVisualizationDestroyRequestLabel(), 90), + VisualizationLabelDto.of( + properties.getProgressVisualizationDestroyResponseLabel(), 100))); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isEmpty() { + return stepsQueue.isEmpty(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isNext() { + mutex.lock(); + + try { + return !batchQueue.isEmpty(); + } finally { + mutex.unlock(); + } + } + + /** + * @see IVisualizationLabel + */ + @Override + public void pushNext() { + mutex.lock(); + + batchQueue.push(stepsQueue.pop().toString()); + + mutex.unlock(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public String getCurrent() { + mutex.lock(); + + try { + return batchQueue.pollLast(); + } finally { + mutex.unlock(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/common/label/VersionCommandVisualizationLabel.java b/cli/src/main/java/com/repoachiever/service/visualization/common/label/VersionCommandVisualizationLabel.java new file mode 100644 index 0000000..3b88448 --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/common/label/VersionCommandVisualizationLabel.java @@ -0,0 +1,77 @@ +package com.repoachiever.service.visualization.common.label; + +import com.repoachiever.dto.VisualizationLabelDto; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.visualization.common.IVisualizationLabel; +import java.util.ArrayDeque; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents label set used for version command service. */ +@Service +public class VersionCommandVisualizationLabel implements IVisualizationLabel { + private final ArrayDeque stepsQueue = new ArrayDeque<>(); + + private final ArrayDeque batchQueue = new ArrayDeque<>(); + + private final ReentrantLock mutex = new ReentrantLock(); + + public VersionCommandVisualizationLabel(@Autowired PropertiesEntity properties) { + stepsQueue.addAll( + List.of( + VisualizationLabelDto.of( + properties.getProgressVisualizationHealthCheckRequestLabel(), 10), + VisualizationLabelDto.of( + properties.getProgressVisualizationVersionInfoRequestLabel(), 100))); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isEmpty() { + return stepsQueue.isEmpty(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public boolean isNext() { + mutex.lock(); + + try { + return !batchQueue.isEmpty(); + } finally { + mutex.unlock(); + } + } + + /** + * @see IVisualizationLabel + */ + @Override + public void pushNext() { + mutex.lock(); + + batchQueue.push(stepsQueue.pop().toString()); + + mutex.unlock(); + } + + /** + * @see IVisualizationLabel + */ + @Override + public String getCurrent() { + mutex.lock(); + + try { + return batchQueue.pollLast(); + } finally { + mutex.unlock(); + } + } +} diff --git a/cli/src/main/java/com/repoachiever/service/visualization/state/VisualizationState.java b/cli/src/main/java/com/repoachiever/service/visualization/state/VisualizationState.java new file mode 100644 index 0000000..a8b6b0f --- /dev/null +++ b/cli/src/main/java/com/repoachiever/service/visualization/state/VisualizationState.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.visualization.state; + +import com.repoachiever.service.visualization.common.IVisualizationLabel; +import java.util.*; +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Service; + +/** + * Represents general visualization state used to gather output values to be processed by + * visualization service. + */ +@Getter +@Service +public class VisualizationState { + @Setter private IVisualizationLabel label; + + private final List result = new ArrayList<>(); + + /** + * Adds given message to the result array. + * + * @param message given message to be added to result array. + */ + public void addResult(String message) { + result.add(message); + } +} diff --git a/cli/src/main/resources/application.properties b/cli/src/main/resources/application.properties new file mode 100644 index 0000000..07e434b --- /dev/null +++ b/cli/src/main/resources/application.properties @@ -0,0 +1,43 @@ +# Describes Spring related properties. +spring.main.banner-mode=off +spring.main.web-application-type=NONE + +# Describes the path and name of a configuration file. +config.root=.resourcetracker/config +config.user.file=user.yaml + +# Describes visualizer state update period +progress.visualization.period=1000 + +# Describes visualization label used for secrets validation request process. +progress.visualization.secrets-acquire-request=Checking if the given cloud credentials are valid + +# Describes visualization label used for script validation request process. +progress.visualization.script-acquire-request=Checking if the given script is allowed to be used + +# Describes visualization label used for infrastructure deployment request process. +progress.visualization.apply-request=Sending infrastructure deployment request + +# Describes visualization label used for infrastructure deployment response. +progress.visualization.apply-response=Application of deployment infrastructure has been completed + +# Describes visualization label used for infrastructure destruction request process. +progress.visualization.destroy-request=Sending infrastructure destruction request + +# Describes visualization label used for infrastructure destruction response. +progress.visualization.destroy-response=Destruction of deployed infrastructure has been completed + +# Describes visualization label used for general state retrieval request process. +progress.visualization.state-request=Sending general state retrieval request + +# Describes visualization label used for general state retrieval response. +progress.visualization.state-response=General state has been retrieved successfully + +# Describes visualization label used for version request process. +progress.visualization.version-info-request=Sending request to API Server to retrieve its version information + +# Describes visualization label used for health check request. +progress.visualization.health-check-request=Sending health check request + +# Describes visualization label used for readiness check request. +progress.visualization.readiness-check-request=Sending readiness check request \ No newline at end of file diff --git a/cli/src/main/resources/log4j2.xml b/cli/src/main/resources/log4j2.xml new file mode 100644 index 0000000..e47463a --- /dev/null +++ b/cli/src/main/resources/log4j2.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + %d %p %c{1.} [%t] %m%n + + + + + + + + + + + + + + + diff --git a/cluster/pom.xml b/cluster/pom.xml new file mode 100644 index 0000000..c8c484a --- /dev/null +++ b/cluster/pom.xml @@ -0,0 +1,176 @@ + + 4.0.0 + cluster + 1.0-SNAPSHOT + cluster + Worker for RepoAchiever + + + com.repoachiever + base + 1.0-SNAPSHOT + + + + true + allow + + com.repoachiever.Cluster + + + + + + + Shell-Command-Executor-Lib + Shell-Command-Executor-Lib + system + ${basedir}/../lib/Shell-Command-Executor-Lib-0.5.0-SNAPSHOT.jar + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-log4j2 + + + org.springframework.integration + spring-integration-rmi + + + org.springframework + spring-remoting + + + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.projectlombok + lombok + + + org.yaml + snakeyaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + commons-io + commons-io + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + + + junit + junit + + + + + cluster + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + com.google.cloud.tools + jib-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 19 + 19 + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + + + + ${basedir}/target/cluster.jar + ${main.basedir}/../bin/cluster/cluster.jar + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + + + + + dev + + true + + + dev + + + + prod + + prod + + + + diff --git a/cluster/src/main/java/com/repoachiever/App.java b/cluster/src/main/java/com/repoachiever/App.java new file mode 100644 index 0000000..6a011e4 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/App.java @@ -0,0 +1,37 @@ +package com.repoachiever; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.apiserver.resource.ApiServerCommunicationResource; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.integration.communication.cluster.ClusterCommunicationConfigService; +import com.repoachiever.service.integration.logging.state.LoggingStateService; +import com.repoachiever.service.executor.CommandExecutorService; +import com.repoachiever.service.integration.logging.transfer.LoggingTransferService; +import com.repoachiever.service.waiter.WaiterHelper; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; + +/** + * Represents initialization point for the RepoAchiever Cluster application. + */ +@Component +@Import({ + ConfigService.class, + CommandExecutorService.class, + PropertiesEntity.class, + ClusterCommunicationConfigService.class, + ApiServerCommunicationResource.class, + LoggingStateService.class, + LoggingTransferService.class +}) +public class App implements ApplicationRunner { + /** + * @see ApplicationRunner + */ + @Override + public void run(ApplicationArguments args) { + WaiterHelper.waitForExit(); + } +} diff --git a/cluster/src/main/java/com/repoachiever/Cluster.java b/cluster/src/main/java/com/repoachiever/Cluster.java new file mode 100644 index 0000000..ef757f8 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/Cluster.java @@ -0,0 +1,15 @@ +package com.repoachiever; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Represents entry point for the RepoAchiever Cluster application. + */ +@SpringBootApplication +public class Cluster { + public static void main(String[] args) { + SpringApplication application = new SpringApplication(App.class); + System.exit(SpringApplication.exit(application.run(args))); + } +} diff --git a/cluster/src/main/java/com/repoachiever/converter/CronExpressionConverter.java b/cluster/src/main/java/com/repoachiever/converter/CronExpressionConverter.java new file mode 100644 index 0000000..24b4451 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/converter/CronExpressionConverter.java @@ -0,0 +1,28 @@ +package com.repoachiever.converter; + +import com.repoachiever.exception.CronExpressionException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; +import org.springframework.scheduling.support.CronExpression; + +/** + * Represents converter used for cron expression parsing operation. + */ +public class CronExpressionConverter { + /** + * Converts frequency from cron expression to milliseconds. + * + * @param src cron expression to be converted + * @return frequency in milliseconds + */ + public static Long convert(String src) throws CronExpressionException { + CronExpression cronExpression = CronExpression.parse(src); + LocalDateTime nextExecutionTime = cronExpression.next(LocalDateTime.now()); + if (Objects.isNull(nextExecutionTime)) { + throw new CronExpressionException(); + } + LocalDateTime afterNextExecutionTime = cronExpression.next(nextExecutionTime); + return Duration.between(nextExecutionTime, afterNextExecutionTime).toMillis(); + } +} diff --git a/cluster/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java b/cluster/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java new file mode 100644 index 0000000..0051755 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents gathered output of the executed command. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class CommandExecutorOutputDto { + private String normalOutput; + + private String errorOutput; +} diff --git a/cluster/src/main/java/com/repoachiever/entity/ConfigEntity.java b/cluster/src/main/java/com/repoachiever/entity/ConfigEntity.java new file mode 100644 index 0000000..790ce5f --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/entity/ConfigEntity.java @@ -0,0 +1,170 @@ +package com.repoachiever.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Service used to perform RepoAchiever Cluster processing operation. + */ +@Getter +public class ConfigEntity { + /** + * Contains metadata for a specific RepoAchiever Cluster allocation. + */ + @Getter + public static class Metadata { + @JsonProperty("name") + public String name; + + @JsonProperty("workspace_unit_key") + public String workspaceUnitKey; + } + + @Valid + @NotNull + @JsonProperty("metadata") + public Metadata metadata; + + /** + * Represents filter section elected for a specific RepoAchiever Cluster allocation. + */ + @Getter + public static class Filter { + @JsonProperty("locations") + public List locations; + } + + @Valid + @NotNull + @JsonProperty("filter") + public Filter filter; + + /** + * Represents external service configurations for RepoAchiever Cluster allocation used to retrieve content. + */ + @Getter + public static class Service { + /** + * Represents all supported service providers, which can be used by RepoAchiever Cluster allocation. + */ + public enum Provider { + LOCAL("git-local"), + GITHUB("git-github"); + + private final String value; + + Provider(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + @JsonProperty("provider") + public Provider provider; + + /** + * Represents credentials used for external service communication by RepoAchiever Cluster allocation. + */ + @Getter + public static class Credentials { + @JsonProperty("token") + public String token; + } + + @JsonProperty("credentials") + public Credentials credentials; + } + + @Valid + @NotNull + @JsonProperty("service") + public Service service; + + /** Represents RepoAchiever Cluster configuration used for internal communication infrastructure setup. */ + @Getter + public static class Communication { + @NotNull + @JsonProperty("api_server_name") + public String apiServerName; + + @NotNull + @JsonProperty("port") + public Integer port; + } + + @Valid + @NotNull + @JsonProperty("communication") + public Communication communication; + + /** Represents RepoAchiever Cluster configuration used for content management. */ + @Getter + public static class Content { + @NotNull + @Pattern(regexp = "(^zip$)|(^tar$)") + @JsonProperty("format") + public String format; + } + + @Valid + @NotNull + @JsonProperty("content") + public Content content; + + /** + * Represents RepoAchiever API Server resources configuration section. + */ + @Getter + public static class Resource { + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Cluster. + */ + @Getter + public static class Cluster { + @NotNull + @JsonProperty("max-workers") + public Integer maxWorkers; + } + + @Valid + @NotNull + @JsonProperty("cluster") + public Cluster cluster; + + /** + * Represents RepoAchiever API Server configuration used for RepoAchiever Worker. + */ + @Getter + public static class Worker { + @NotNull + @Pattern( + regexp = + "(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\\?])|([\\*]))[\\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\\?])|([\\*]))[\\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\\?])|([\\*]))[\\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\\?])|([\\*]))([\\s]?(([\\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?|" + + " (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?|" + + " ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)") + @JsonProperty("frequency") + public String frequency; + } + + @Valid + @NotNull + @JsonProperty("worker") + public Worker worker; + } + + @Valid + @NotNull + @JsonProperty("resource") + public Resource resource; +} diff --git a/cluster/src/main/java/com/repoachiever/entity/PropertiesEntity.java b/cluster/src/main/java/com/repoachiever/entity/PropertiesEntity.java new file mode 100644 index 0000000..223d3af --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/entity/PropertiesEntity.java @@ -0,0 +1,56 @@ +package com.repoachiever.entity; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.io.ClassPathResource; +import org.apache.commons.lang3.StringUtils; + +/** + * Exposes access to properties setup to be used for further configuration. + */ +@Getter +@Configuration +public class PropertiesEntity { + private static final String GIT_CONFIG_PROPERTIES_FILE = "git.properties"; + + @Value(value = "${REPOACHIEVER_CLUSTER_CONTEXT:null}") + private String clusterContext; + + @Value(value = "${logging.transfer.frequency}") + private Integer loggingTransferFrequency; + + @Value(value = "${logging.state.frequency}") + private Integer loggingStateFrequency; + + @Value(value = "${logging.state-finalizer.frequency}") + private Integer loggingStateFinalizerFrequency; + + @Value(value = "${git.commit.id.abbrev}") + private String gitCommitId; + + /** + * Adds custom properties to resource configurations. + * + * @return modified property sources configurer. + */ + @Bean + private static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { + PropertySourcesPlaceholderConfigurer propsConfig = new PropertySourcesPlaceholderConfigurer(); + propsConfig.setLocation(new ClassPathResource(GIT_CONFIG_PROPERTIES_FILE)); + propsConfig.setIgnoreResourceNotFound(true); + propsConfig.setIgnoreUnresolvablePlaceholders(true); + return propsConfig; + } + + /** + * Removes the last symbol in git commit id of the repository. + * + * @return chopped repository git commit id. + */ + public String getGitCommitId() { + return StringUtils.chop(gitCommitId); + } +} diff --git a/cluster/src/main/java/com/repoachiever/exception/ApiServerOperationFailureException.java b/cluster/src/main/java/com/repoachiever/exception/ApiServerOperationFailureException.java new file mode 100644 index 0000000..3376590 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/ApiServerOperationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when RepoAchiever API Server operation fails. + */ +public class ApiServerOperationFailureException extends IOException { + public ApiServerOperationFailureException() { + this(""); + } + + public ApiServerOperationFailureException(Object... message) { + super( + new Formatter() + .format("RepoAchiever API Server operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cluster/src/main/java/com/repoachiever/exception/CommandExecutorException.java b/cluster/src/main/java/com/repoachiever/exception/CommandExecutorException.java new file mode 100644 index 0000000..1033e07 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/CommandExecutorException.java @@ -0,0 +1,17 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used to indicate command executor failure. + */ +public class CommandExecutorException extends IOException { + public CommandExecutorException(Object... message) { + super( + new Formatter() + .format("Invalid command executor behaviour: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cluster/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java b/cluster/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java new file mode 100644 index 0000000..a6672f1 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/CommunicationConfigurationFailureException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when communication configuration fails. + */ +public class CommunicationConfigurationFailureException extends IOException { + public CommunicationConfigurationFailureException() { + this(""); + } + + public CommunicationConfigurationFailureException(Object... message) { + super( + new Formatter() + .format("Communication configuration operation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cluster/src/main/java/com/repoachiever/exception/ConfigNotGivenException.java b/cluster/src/main/java/com/repoachiever/exception/ConfigNotGivenException.java new file mode 100644 index 0000000..8ad7455 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/ConfigNotGivenException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when config file is not provided. + */ +public class ConfigNotGivenException extends IOException { + public ConfigNotGivenException() { + this(""); + } + + public ConfigNotGivenException(Object... message) { + super( + new Formatter() + .format("Config file is not given: %s", Arrays.stream(message).toArray()) + .toString()); + } +} \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/exception/ConfigValidationException.java b/cluster/src/main/java/com/repoachiever/exception/ConfigValidationException.java new file mode 100644 index 0000000..aa2f396 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/ConfigValidationException.java @@ -0,0 +1,21 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used when config file is not valid. + */ +public class ConfigValidationException extends IOException { + public ConfigValidationException() { + this(""); + } + + public ConfigValidationException(Object... message) { + super( + new Formatter() + .format("Config file content is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cluster/src/main/java/com/repoachiever/exception/CronExpressionException.java b/cluster/src/main/java/com/repoachiever/exception/CronExpressionException.java new file mode 100644 index 0000000..d2befeb --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/exception/CronExpressionException.java @@ -0,0 +1,17 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** + * Represents exception used to indicate cron expression conversion failure. + */ +public class CronExpressionException extends IOException { + public CronExpressionException(Object... message) { + super( + new Formatter() + .format("Invalid cron exception: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/cluster/src/main/java/com/repoachiever/logging/FatalAppender.java b/cluster/src/main/java/com/repoachiever/logging/FatalAppender.java new file mode 100644 index 0000000..e7a1df3 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/logging/FatalAppender.java @@ -0,0 +1,36 @@ +package com.repoachiever.logging; + +import com.repoachiever.service.state.StateService; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +/** + * Service used for logging fatal level application state changes. + */ +@Plugin(name = "fatalappender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class FatalAppender extends AbstractAppender { + protected FatalAppender(String name, Filter filter) { + super(name, filter, null, false, null); + } + + @PluginFactory + public static FatalAppender createAppender( + @PluginAttribute("name") String name, @PluginElement("Filter") Filter filter) { + return new FatalAppender(name, filter); + } + + @Override + public void append(LogEvent event) { + if (event.getLevel().equals(Level.FATAL)) { + StateService.setExit(true); + } + } +} diff --git a/cluster/src/main/java/com/repoachiever/logging/TransferAppender.java b/cluster/src/main/java/com/repoachiever/logging/TransferAppender.java new file mode 100644 index 0000000..393c5d2 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/logging/TransferAppender.java @@ -0,0 +1,39 @@ +package com.repoachiever.logging; + +import com.repoachiever.logging.common.LoggingConfigurationHelper; +import com.repoachiever.service.state.StateService; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +/** + * Service used for logging message transfer to RepoAchiever API Server allocation. + */ +@Plugin(name = "transferappender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class TransferAppender extends AbstractAppender { + protected TransferAppender(String name, Filter filter) { + super(name, filter, null, false, null); + } + + @PluginFactory + public static TransferAppender createAppender( + @PluginAttribute("name") String name, @PluginElement("Filter") Filter filter) { + return new TransferAppender(name, filter); + } + + @Override + public void append(LogEvent event) { + String message = event.getMessage().getFormattedMessage(); + + if (LoggingConfigurationHelper.isMessageTransferable(message)) { + StateService.addLogMessage( + LoggingConfigurationHelper.extractTransferableMessage(message)); + } + } +} diff --git a/cluster/src/main/java/com/repoachiever/logging/common/LoggingConfigurationHelper.java b/cluster/src/main/java/com/repoachiever/logging/common/LoggingConfigurationHelper.java new file mode 100644 index 0000000..a6d1f00 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/logging/common/LoggingConfigurationHelper.java @@ -0,0 +1,38 @@ +package com.repoachiever.logging.common; + +/** + * Contains helpful tools used for logging configuration. + */ +public class LoggingConfigurationHelper { + private static final String TRANSFERABLE_MESSAGE_PREFIX = "!transferable!"; + + /** + * Checks if the given log message contains given prefix. + * + * @param message given log message. + * @return result of the check. + */ + public static Boolean isMessageTransferable(String message) { + return message.contains(TRANSFERABLE_MESSAGE_PREFIX); + } + + /** + * Formats transferable message with the given prefix. + * + * @param message given formatted transferable log message. + * @return formatted transferable message. + */ + public static String extractTransferableMessage(String message) { + return message.replaceAll(TRANSFERABLE_MESSAGE_PREFIX, ""); + } + + /** + * Formats transferable message with the given prefix. + * + * @param message given log message. + * @return formatted transferable message. + */ + public static String getTransferableMessage(String message) { + return String.format("%s %s", TRANSFERABLE_MESSAGE_PREFIX, message); + } +} diff --git a/cluster/src/main/java/com/repoachiever/resource/communication/ClusterCommunicationResource.java b/cluster/src/main/java/com/repoachiever/resource/communication/ClusterCommunicationResource.java new file mode 100644 index 0000000..7408b2c --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/resource/communication/ClusterCommunicationResource.java @@ -0,0 +1,56 @@ +package com.repoachiever.resource.communication; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.communication.cluster.IClusterCommunicationService; +import com.repoachiever.service.state.StateService; + +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +/** + * Contains implementation of communication provider for RepoAchiever Cluster. + */ +public class ClusterCommunicationResource extends UnicastRemoteObject implements IClusterCommunicationService { + private final PropertiesEntity properties; + + public ClusterCommunicationResource(PropertiesEntity properties) throws RemoteException { + this.properties = properties; + } + + /** + * @see IClusterCommunicationService + */ + @Override + public void performSuspend() throws RemoteException { + StateService.setSuspended(true); + } + + /** + * @see IClusterCommunicationService + */ + @Override + public void performServe() throws RemoteException { + StateService.setSuspended(false); + } + + /** + * @see IClusterCommunicationService + */ + @Override + public Boolean retrieveHealthCheck() throws RemoteException { + return true; + } + + /** + * @see IClusterCommunicationService + */ + @Override + public String retrieveVersion() throws RemoteException { + return properties.getGitCommitId(); + } + + @Override + public Integer retrieveWorkerAmount() throws RemoteException { + return 10; + } +} \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/service/apiserver/resource/ApiServerCommunicationResource.java b/cluster/src/main/java/com/repoachiever/service/apiserver/resource/ApiServerCommunicationResource.java new file mode 100644 index 0000000..d2ab784 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/apiserver/resource/ApiServerCommunicationResource.java @@ -0,0 +1,128 @@ +package com.repoachiever.service.apiserver.resource; + +import com.repoachiever.exception.ApiServerOperationFailureException; +import com.repoachiever.exception.CommunicationConfigurationFailureException; +import com.repoachiever.service.communication.apiserver.IApiServerCommunicationService; +import com.repoachiever.service.communication.common.CommunicationProviderConfigurationHelper; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.Arrays; + +/** + * Represents implementation for RepoAchiever API Server remote API. + */ +@Service +public class ApiServerCommunicationResource { + private static final Logger logger = LogManager.getLogger(ApiServerCommunicationResource.class); + + @Autowired + private ConfigService configService; + + private Registry registry; + + @PostConstruct + private void configure() { + try { + this.registry = LocateRegistry.getRegistry( + configService.getConfig().getCommunication().getPort()); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + } + } + + /** + * Retrieves remote RepoAchiever API Server allocation. + * + * @return retrieved RepoAchiever API Server allocation. + * @throws ApiServerOperationFailureException if RepoAchiever API Server operation fails. + */ + private IApiServerCommunicationService retrieveAllocation() throws ApiServerOperationFailureException { + try { + return (IApiServerCommunicationService) registry.lookup( + CommunicationProviderConfigurationHelper.getBindName( + configService.getConfig().getCommunication().getPort(), + configService.getConfig().getCommunication().getApiServerName())); + } catch (RemoteException | NotBoundException e) { + throw new ApiServerOperationFailureException(e.getMessage()); + } + } + + /** + * Performs raw content upload operation. + * + * @param content given content to be uploaded. + * @throws ApiServerOperationFailureException if RepoAchiever API Server operation fails. + */ + public void performRawContentUpload(InputStream content) throws ApiServerOperationFailureException { + IApiServerCommunicationService allocation = retrieveAllocation(); + + try { + allocation.performRawContentUpload( + configService.getConfig().getMetadata().getWorkspaceUnitKey(), + content); + } catch (RemoteException e) { + throw new ApiServerOperationFailureException(e.getMessage()); + } + } + + /** + * Performs additional content(issues, prs, releases) upload operation, initiated by RepoAchiever Cluster. + * + * @param content given content to be uploaded. + * @throws ApiServerOperationFailureException if RepoAchiever API Server operation fails. + */ + public void performAdditionalContentUpload(String content) throws ApiServerOperationFailureException { + IApiServerCommunicationService allocation = retrieveAllocation(); + + try { + allocation.performAdditionalContentUpload( + configService.getConfig().getMetadata().getWorkspaceUnitKey(), + content); + } catch (RemoteException e) { + throw new ApiServerOperationFailureException(e.getMessage()); + } + } + + /** + * Handles incoming log messages related to the RepoAchiever Cluster allocation. + * + * @param message given RepoAchiever Cluster log message. + * @throws ApiServerOperationFailureException if RepoAchiever API Server operation fails. + */ + public void performLogsTransfer(String message) throws ApiServerOperationFailureException { + IApiServerCommunicationService allocation = retrieveAllocation(); + + try { + allocation.performLogsTransfer( + configService.getConfig().getMetadata().getName(), message); + } catch (RemoteException e) { + throw new ApiServerOperationFailureException(e.getMessage()); + } + } + + /** + * Retrieves health check status of RepoAchiever API Server allocation. + * + * @return result of the check. + * @throws ApiServerOperationFailureException if RepoAchiever API Server operation fails. + */ + public Boolean retrieveHealthCheck() throws ApiServerOperationFailureException { + IApiServerCommunicationService allocation = retrieveAllocation(); + + try { + return allocation.retrieveHealthCheck(); + } catch (RemoteException e) { + throw new ApiServerOperationFailureException(e.getMessage()); + } + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java b/cluster/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java new file mode 100644 index 0000000..e7e7a15 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/communication/apiserver/IApiServerCommunicationService.java @@ -0,0 +1,45 @@ +package com.repoachiever.service.communication.apiserver; + +import java.io.InputStream; +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * Represents communication provider for RepoAchiever API Server. + */ +public interface IApiServerCommunicationService extends Remote { + /** + * Performs raw content upload operation, initiated by RepoAchiever Cluster. + * + * @param workspaceUnitKey given user workspace unit key. + * @param content given content to be uploaded. + * @throws RemoteException if remote request fails. + */ + void performRawContentUpload(String workspaceUnitKey, InputStream content) throws RemoteException; + + /** + * Performs additional content(issues, prs, releases) upload operation, initiated by RepoAchiever Cluster. + * + * @param workspaceUnitKey given user workspace unit key. + * @param content given content to be uploaded. + * @throws RemoteException if remote request fails. + */ + void performAdditionalContentUpload(String workspaceUnitKey, String content) throws RemoteException; + + /** + * Handles incoming log messages related to the given RepoAchiever Cluster allocation. + * + * @param name given RepoAchiever Cluster allocation name. + * @param message given RepoAchiever Cluster log message. + * @throws RemoteException if remote request fails. + */ + void performLogsTransfer(String name, String message) throws RemoteException; + + /** + * Retrieves latest RepoAchiever API Server health check states. + * + * @return RepoAchiever API Server health check status. + * @throws RemoteException if remote request fails. + */ + Boolean retrieveHealthCheck() throws RemoteException; +} \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java b/cluster/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java new file mode 100644 index 0000000..92fc69b --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/communication/cluster/IClusterCommunicationService.java @@ -0,0 +1,50 @@ +package com.repoachiever.service.communication.cluster; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** Represents client for RepoAchiever Cluster remote API. */ +public interface IClusterCommunicationService extends Remote { + /** + * Performs RepoAchiever Cluster suspend operation. Has no effect if RepoAchiever Cluster was + * already suspended previously. + * + * @throws RemoteException if remote request fails. + */ + void performSuspend() throws RemoteException; + + /** + * Performs RepoAchiever Cluster serve operation. Has no effect if RepoAchiever Cluster was not + * suspended previously. + * + * @throws RemoteException if remote request fails. + */ + void performServe() throws RemoteException; + + /** + * Retrieves latest RepoAchiever Cluster health check states. + * + * @return RepoAchiever Cluster health check status. + * @throws RemoteException if remote request fails. + */ + Boolean retrieveHealthCheck() throws RemoteException; + + /** + * Retrieves version of the allocated RepoAchiever Cluster instance allowing to confirm API + * compatability. + * + * @return RepoAchiever Cluster version. + * @throws RemoteException if remote request fails. + */ + String retrieveVersion() throws RemoteException; + + /** + * Retrieves amount of allocated workers. + * + * @return amount of allocated workers. + * @throws RemoteException if remote request fails. + */ + Integer retrieveWorkerAmount() throws RemoteException; +} + +// TODO: LOCATE ALL RMI RELATED CLASSES AT THE SAME PATH \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java b/cluster/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java new file mode 100644 index 0000000..0a5462c --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/communication/common/CommunicationProviderConfigurationHelper.java @@ -0,0 +1,15 @@ +package com.repoachiever.service.communication.common; + +/** Contains helpful tools used for communication provider configuration. */ +public class CommunicationProviderConfigurationHelper { + /** + * Composes binding URI declaration for RMI. + * + * @param registryPort given registry port. + * @param suffix given binding suffix. + * @return composed binding URI declaration for RMI. + */ + public static String getBindName(Integer registryPort, String suffix) { + return String.format("//localhost:%d/%s", registryPort, suffix); + } +} \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/service/config/ConfigService.java b/cluster/src/main/java/com/repoachiever/service/config/ConfigService.java new file mode 100644 index 0000000..3f94c23 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/config/ConfigService.java @@ -0,0 +1,106 @@ +package com.repoachiever.service.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ConfigNotGivenException; +import com.repoachiever.exception.ConfigValidationException; +import jakarta.annotation.PostConstruct; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Service used to perform RepoAchiever Cluster configuration processing operation. + */ +@Component +public class ConfigService { + private static final Logger logger = LogManager.getLogger(ConfigService.class); + + @Autowired + private PropertiesEntity properties; + + private ConfigEntity parsedConfigFile; + + /** + * Performs configuration file parsing operation. + */ + @PostConstruct + private void configure() { + String clusterContext = properties.getClusterContext(); + + if (Objects.equals(clusterContext, "null")) { + logger.fatal(new ConfigNotGivenException().getMessage()); + return; + } + + InputStream configFile = null; + + try { + configFile = IOUtils.toInputStream(clusterContext, "UTF-8"); + + ObjectMapper mapper = + new ObjectMapper(new JsonFactory()) + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectReader reader = mapper.reader().forType(new TypeReference() { + }); + + try { + parsedConfigFile = reader.readValues(configFile).readAll().getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + return; + } + + try (ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory()) { + Validator validator = validatorFactory.getValidator(); + + Set> validationResult = + validator.validate(parsedConfigFile); + + if (!validationResult.isEmpty()) { + logger.fatal(new ConfigValidationException( + validationResult.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", "))).getMessage()); + } + } + } finally { + try { + configFile.close(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } + } + + /** + * Retrieves parsed configuration file entity. + * + * @return retrieved parsed configuration file entity. + */ + public ConfigEntity getConfig() { + return parsedConfigFile; + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java b/cluster/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java new file mode 100644 index 0000000..8de0220 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/executor/CommandExecutorService.java @@ -0,0 +1,59 @@ +package com.repoachiever.service.executor; + +import com.repoachiever.dto.CommandExecutorOutputDto; +import com.repoachiever.exception.CommandExecutorException; +import java.io.IOException; +import org.springframework.stereotype.Service; +import process.SProcess; +import process.SProcessExecutor; +import process.exceptions.NonMatchingOSException; +import process.exceptions.SProcessNotYetStartedException; + +/** CommandExecutorService provides command execution service. */ +@Service +public class CommandExecutorService { + private final SProcessExecutor processExecutor; + + CommandExecutorService() { + this.processExecutor = SProcessExecutor.getCommandExecutor(); + } + + /** + * Executes given command and gathers its output. + * + * @param command command to be executed + * @return CommandExecutorOutputEntity output, which consists of both stdout and stderr + * @throws CommandExecutorException when command execution fails or output is not gathered + */ + public CommandExecutorOutputDto executeCommand(SProcess command) throws CommandExecutorException { + try { + processExecutor.executeCommand(command); + } catch (IOException | NonMatchingOSException e) { + throw new CommandExecutorException(e.getMessage()); + } + + try { + command.waitForCompletion(); + } catch (SProcessNotYetStartedException | InterruptedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + String commandErrorOutput; + + try { + commandErrorOutput = command.getErrorOutput(); + } catch (SProcessNotYetStartedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + String commandNormalOutput; + + try { + commandNormalOutput = command.getNormalOutput(); + } catch (SProcessNotYetStartedException e) { + throw new CommandExecutorException(e.getMessage()); + } + + return CommandExecutorOutputDto.of(commandNormalOutput, commandErrorOutput); + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/integration/communication/cluster/ClusterCommunicationConfigService.java b/cluster/src/main/java/com/repoachiever/service/integration/communication/cluster/ClusterCommunicationConfigService.java new file mode 100644 index 0000000..9a6b8d0 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/integration/communication/cluster/ClusterCommunicationConfigService.java @@ -0,0 +1,58 @@ +package com.repoachiever.service.integration.communication.cluster; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.CommunicationConfigurationFailureException; +import com.repoachiever.resource.communication.ClusterCommunicationResource; +import com.repoachiever.service.config.ConfigService; +import com.repoachiever.service.communication.common.CommunicationProviderConfigurationHelper; +import jakarta.annotation.PostConstruct; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +/** + * Service used to perform RepoAchiever Cluster communication provider configuration. + */ +@Component +public class ClusterCommunicationConfigService { + private static final Logger logger = LogManager.getLogger(ClusterCommunicationConfigService.class); + + @Autowired + private ConfigService configService; + + @Autowired + private PropertiesEntity properties; + + /** + * Performs setup of RepoAchiever Cluster communication provider. + */ + @PostConstruct + private void process() { + Registry registry; + + try { + registry = LocateRegistry.getRegistry( + configService.getConfig().getCommunication().getPort()); + } catch (RemoteException e) { + logger.fatal(new CommunicationConfigurationFailureException(e.getMessage()).getMessage()); + return; + } + + Thread.ofPlatform().start(() -> { + try { + registry.rebind( + CommunicationProviderConfigurationHelper.getBindName( + configService.getConfig().getCommunication().getPort(), + configService.getConfig().getMetadata().getName()), + new ClusterCommunicationResource(properties)); + } catch (RemoteException e) { + logger.fatal(e.getMessage()); + } + }); + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/integration/logging/state/LoggingStateService.java b/cluster/src/main/java/com/repoachiever/service/integration/logging/state/LoggingStateService.java new file mode 100644 index 0000000..66e3bdf --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/integration/logging/state/LoggingStateService.java @@ -0,0 +1,57 @@ +package com.repoachiever.service.integration.logging.state; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.state.StateService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.concurrent.*; + +/** + * Service used to handle incoming logging related Ä…pplication state changes. + */ +@Component +public class LoggingStateService { + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private PropertiesEntity properties; + + private final ScheduledExecutorService scheduledExecutorService = + Executors.newScheduledThreadPool(2); + + /** + * Performs application exit if the required state has been changed. + */ + @PostConstruct + private void process() { + scheduledExecutorService.scheduleAtFixedRate(() -> { + if (StateService.getExit()) { + CountDownLatch finalizer = new CountDownLatch(1); + + ScheduledFuture finalizerFeature = + scheduledExecutorService.scheduleAtFixedRate(() -> { + if (StateService.getLogMessagesQueue().isEmpty()) { + finalizer.countDown(); + } + }, 0, + properties.getLoggingStateFinalizerFrequency(), + TimeUnit.MILLISECONDS); + + try { + finalizer.await(); + } catch (InterruptedException ignored) { + } + + finalizerFeature.cancel(true); + + ((ConfigurableApplicationContext) applicationContext).close(); + System.exit(1); + } + }, 0, properties.getLoggingStateFrequency(), TimeUnit.MILLISECONDS); + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/integration/logging/transfer/LoggingTransferService.java b/cluster/src/main/java/com/repoachiever/service/integration/logging/transfer/LoggingTransferService.java new file mode 100644 index 0000000..a7f4cde --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/integration/logging/transfer/LoggingTransferService.java @@ -0,0 +1,53 @@ +package com.repoachiever.service.integration.logging.transfer; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ApiServerOperationFailureException; +import com.repoachiever.logging.common.LoggingConfigurationHelper; +import com.repoachiever.service.apiserver.resource.ApiServerCommunicationResource; +import com.repoachiever.service.state.StateService; +import jakarta.annotation.PostConstruct; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.*; + +/** + * Service used to handle incoming logging messages to be transferred to RepoAchiever API Server allocation. + */ +@Component +public class LoggingTransferService { + private static final Logger logger = LogManager.getLogger(LoggingTransferService.class); + + @Autowired + private PropertiesEntity properties; + + @Autowired + private ApiServerCommunicationResource apiServerCommunicationResource; + + private final ScheduledExecutorService scheduledExecutorService = + Executors.newScheduledThreadPool(2); + + /** + * Performs application logs transfer to RepoAchiever API Server allocation. + */ + @PostConstruct + private void process() { + logger.info(LoggingConfigurationHelper.getTransferableMessage("it works")); + + scheduledExecutorService.scheduleAtFixedRate(() -> { + if (!StateService.getExit()) { + while (!StateService.getLogMessagesQueue().isEmpty()) { + try { + apiServerCommunicationResource.performLogsTransfer( + StateService.getLogMessagesQueue().poll()); + + } catch (ApiServerOperationFailureException e) { + logger.fatal(e.getMessage()); + } + } + } + }, 0, properties.getLoggingTransferFrequency(), TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/cluster/src/main/java/com/repoachiever/service/state/StateService.java b/cluster/src/main/java/com/repoachiever/service/state/StateService.java new file mode 100644 index 0000000..e41bdb6 --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/state/StateService.java @@ -0,0 +1,42 @@ +package com.repoachiever.service.state; + +import lombok.Getter; +import lombok.Setter; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Service used to operate as a collection of application state properties. + */ +public class StateService { + /** + * Represents exit state used to indicate requested application shutdown. + */ + @Getter + @Setter + private static Boolean exit = false; + + /** + * Represents suspended state used to temporary halt execution of RepoAchiever Cluster allocation. By default + * RepoAchiever Cluster is considered to be suspended. + */ + @Getter + @Setter + private static Boolean suspended = true; + + /** + * Represents log message queue used to handle RepoAchiever API Server log message transfer. + */ + @Getter + private final static ConcurrentLinkedQueue logMessagesQueue = new ConcurrentLinkedQueue<>(); + + /** + * Adds new log message to log message queue. + * + * @param message given log message to be added. + */ + public static void addLogMessage(String message) { + logMessagesQueue.add(message); + } +} diff --git a/cluster/src/main/java/com/repoachiever/service/waiter/WaiterHelper.java b/cluster/src/main/java/com/repoachiever/service/waiter/WaiterHelper.java new file mode 100644 index 0000000..ef8d99f --- /dev/null +++ b/cluster/src/main/java/com/repoachiever/service/waiter/WaiterHelper.java @@ -0,0 +1,17 @@ +package com.repoachiever.service.waiter; + +import java.util.concurrent.CountDownLatch; + +/** Represents waiter helper for general usage. */ +public class WaiterHelper { + private static final CountDownLatch latch = new CountDownLatch(1); + + /** Indefinitely waits for manual program execution. */ + public static void waitForExit() { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/cluster/src/main/resources/application.properties b/cluster/src/main/resources/application.properties new file mode 100644 index 0000000..be1e7f5 --- /dev/null +++ b/cluster/src/main/resources/application.properties @@ -0,0 +1,12 @@ +# Describes Spring related properties. +spring.main.banner-mode=off +spring.main.web-application-type=NONE + +# Describes frequency used to perform logs transfers to RepoAchiever API Server allocation. +logging.transfer.frequency=500 + +# Describes frequency used to perform logging state check. +logging.state.frequency=10 + +# Describes frequency used to perform logging state finalizer check. +logging.state-finalizer.frequency=10 \ No newline at end of file diff --git a/cluster/src/main/resources/log4j2.xml b/cluster/src/main/resources/log4j2.xml new file mode 100644 index 0000000..026a4f3 --- /dev/null +++ b/cluster/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/grafana/dashboards/dashboard.yml b/config/grafana/dashboards/dashboard.yml new file mode 100644 index 0000000..60d2246 --- /dev/null +++ b/config/grafana/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/config/grafana/dashboards/diagnostics.tmpl b/config/grafana/dashboards/diagnostics.tmpl new file mode 100644 index 0000000..1214948 --- /dev/null +++ b/config/grafana/dashboards/diagnostics.tmpl @@ -0,0 +1,1831 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "RepoAchiever API Server: ${(info.version)}", + "editable": true, + "gnetId": 179, + "graphTooltip": 1, + "id": 1, + "iteration": 1571330223815, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 17, + "panels": [], + "title": "Host Info", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Default", + "decimals": null, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 15, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "time() - process_start_time_seconds{job=\"prometheus\"}", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Default", + "format": "short", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 35, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "count(count(node_cpu_seconds_total{instance=~\"$node\", mode='system'}) by (cpu))", + "instant": true, + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "CPU Cores", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "Default", + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 6, + "y": 1 + }, + "id": 13, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(ALERTS)", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "0,1", + "title": "Alerts", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "0" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "#d44a3a", + "rgba(237, 129, 40, 0.89)", + "#299c46" + ], + "datasource": "Default", + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 11, + "y": 1 + }, + "id": 11, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(up)", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "0,1", + "title": "Targets Online", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#d44a3a", + "rgba(237, 129, 40, 0.89)", + "#299c46" + ], + "datasource": "Default", + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 15, + "y": 1 + }, + "id": 31, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "count(rate(container_last_seen{job=\"cadvisor\", name!=\"\"}[5m]))", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": "0,1", + "title": "Running Containers", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": null, + "decimals": null, + "format": "decbytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 3, + "y": 5 + }, + "id": 37, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "node_memory_MemTotal_bytes{instance=~\"$node\"}", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Host Memory", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Default", + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 4, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "(sum(node_memory_MemTotal_bytes) - sum(node_memory_MemFree_bytes +node_memory_Buffers_bytes + node_memory_Cached_bytes) ) / sum(node_memory_MemTotal_bytes) * 100", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "65, 90", + "title": "Memory usage", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Default", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 6, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "100 - (avg(irate(node_cpu_seconds_total{instance=~\"$node\",mode=\"idle\"}[5m])) * 100)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A", + "step": 10 + } + ], + "thresholds": "65, 90", + "title": "CPU usage", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "Default", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 12, + "y": 8 + }, + "id": 7, + "interval": null, + "isNew": true, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "options": {}, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "avg( node_filesystem_avail_bytes {mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"})", + "interval": "10s", + "intervalFactor": 1, + "metric": "", + "refId": "A", + "step": 10 + } + ], + "thresholds": "65, 90", + "title": "Filesystem usage", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": { + "RECEIVE": "#ea6460", + "SENT": "#1f78c1", + "TRANSMIT": "#1f78c1" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "fill": 4, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 6, + "x": 0, + "y": 14 + }, + "id": 25, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(container_network_receive_bytes_total{id=\"/\"}[$interval])) by (id)", + "format": "time_series", + "interval": "2m", + "intervalFactor": 2, + "legendFormat": "RECEIVE", + "refId": "A" + }, + { + "expr": "- sum(rate(container_network_transmit_bytes_total{id=\"/\"}[$interval])) by (id)", + "format": "time_series", + "interval": "2m", + "intervalFactor": 2, + "legendFormat": "TRANSMIT", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Node Network Traffic", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "Available Memory": "#508642", + "Used Memory": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "fill": 3, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 6, + "x": 6, + "y": 14 + }, + "id": 27, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(node_memory_MemTotal_bytes) - sum(node_memory_MemAvailable_bytes)", + "format": "time_series", + "interval": "2m", + "intervalFactor": 2, + "legendFormat": "Used Memory", + "refId": "B" + }, + { + "expr": "sum(node_memory_MemAvailable_bytes)", + "format": "time_series", + "interval": "2m", + "intervalFactor": 2, + "legendFormat": "Available Memory", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Node Mermory", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "Available Memory": "#508642", + "Free Storage": "#447ebc", + "Total Storage Available": "#508642", + "Used Memory": "#bf1b00", + "Used Storage": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "fill": 3, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 7, + "x": 12, + "y": 14 + }, + "id": 28, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(node_filesystem_free_bytes {job=\"node-exporter\", instance=~\".*${(nodeexporter.port)}\", device=~\"/dev/.*\", mountpoint!=\"/var/lib/docker/aufs\"}) ", + "format": "time_series", + "interval": "2m", + "intervalFactor": 2, + "legendFormat": "Free Storage", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Filesystem Available", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "decbytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 19, + "panels": [], + "repeat": null, + "title": "Container Performance", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "decimals": 3, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 10, + "w": 6, + "x": 0, + "y": 24 + }, + "id": 3, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(container_cpu_usage_seconds_total{image!=\"\"}[1m])) by (id,name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ name }}", + "metric": "container_cpu_user_seconds_total", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Container CPU usage", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percentunit", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 10, + "w": 6, + "x": 6, + "y": 24 + }, + "id": 2, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "container_memory_max_usage_bytes{image!=\"\"}", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Container Memory Usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "columns": [], + "datasource": "Default", + "fontSize": "100%", + "gridPos": { + "h": 13, + "w": 10, + "x": 12, + "y": 24 + }, + "id": 23, + "links": [], + "options": {}, + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "expr": "ALERTS", + "format": "table", + "intervalFactor": 1, + "refId": "A" + } + ], + "title": "Alerts", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 14, + "w": 6, + "x": 0, + "y": 34 + }, + "id": 8, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sort_desc(sum by (name) (rate(container_network_receive_bytes_total{image!=\"\"}[1m] ) ))", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ name }}", + "metric": "container_network_receive_bytes_total", + "refId": "A", + "step": 10 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Container Network Input", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Default", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 14, + "w": 6, + "x": 6, + "y": 34 + }, + "id": 9, + "isNew": true, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sort_desc(sum by (name) (rate(container_network_transmit_bytes_total{image!=\"\"}[1m] ) ))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{ name }}", + "metric": "container_network_transmit_bytes_total", + "refId": "B", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Container Network Output", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "columns": [], + "datasource": "Default", + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 10, + "x": 12, + "y": 37 + }, + "id": 30, + "links": [], + "options": {}, + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "link": false, + "linkUrl": "", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "expr": "cadvisor_version_info", + "format": "table", + "instant": false, + "interval": "15m", + "intervalFactor": 2, + "legendFormat": "cAdvisor Version: {{cadvisorVersion}}", + "refId": "A" + }, + { + "expr": "prometheus_build_info", + "format": "table", + "interval": "15m", + "intervalFactor": 2, + "legendFormat": "Prometheus Version: {{version}}", + "refId": "B" + }, + { + "expr": "node_exporter_build_info", + "format": "table", + "interval": "15m", + "intervalFactor": 2, + "legendFormat": "Node-Exporter Version: {{version}}", + "refId": "C" + } + ], + "title": "Running Versions", + "transform": "table", + "type": "table" + } + ], + "refresh": "10s", + "schemaVersion": 20, + "style": "dark", + "tags": [ + "docker", + "prometheus, ", + "node-exporter", + "cadvisor" + ], + "templating": { + "list": [ + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m" + }, + "hide": 0, + "label": "interval", + "name": "interval", + "options": [ + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Default", + "definition": "label_values(node_exporter_build_info{name=~'$name'},instance)", + "hide": 0, + "includeAll": true, + "label": "IP", + "multi": true, + "name": "node", + "options": [], + "query": "label_values(node_exporter_build_info{name=~'$name'},instance)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Default", + "definition": "label_values(node_exporter_build_info,env)", + "hide": 0, + "includeAll": true, + "label": "Env", + "multi": true, + "name": "env", + "options": [], + "query": "label_values(node_exporter_build_info,env)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Default", + "definition": "label_values(node_exporter_build_info{env=~'$env'},name)", + "hide": 0, + "includeAll": true, + "label": "CPU Name", + "multi": true, + "name": "name", + "options": [], + "query": "label_values(node_exporter_build_info{env=~'$env'},name)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "RepoAchiever Diagnostics", + "uid": "64nrElFmk", + "version": 2 +} \ No newline at end of file diff --git a/config/grafana/datasources/datasource.tmpl b/config/grafana/datasources/datasource.tmpl new file mode 100644 index 0000000..064ca10 --- /dev/null +++ b/config/grafana/datasources/datasource.tmpl @@ -0,0 +1,19 @@ +apiVersion: 1 + +deleteDatasources: + - name: Default + orgId: 1 + +datasources: + - name: Default + type: prometheus + access: proxy + orgId: 1 + url: http://${(prometheus.host)}:${(prometheus.port)} + isDefault: true + jsonData: + graphiteVersion: "1.1" + tlsAuth: false + tlsAuthWithCACert: false + version: 1 + editable: false diff --git a/config/prometheus/prometheus.tmpl b/config/prometheus/prometheus.tmpl new file mode 100644 index 0000000..f2ded30 --- /dev/null +++ b/config/prometheus/prometheus.tmpl @@ -0,0 +1,18 @@ +global: + scrape_interval: 5s + evaluation_interval: 5s + +scrape_configs: + - job_name: 'prometheus' + + scrape_interval: 15s + + static_configs: + - targets: ['${(metrics.host)}:${(metrics.port)}'] + + - job_name: 'node-exporter' + + scrape_interval: 15s + + static_configs: + - targets: ['${(nodeexporter.host)}:${(nodeexporter.port)}'] diff --git a/docs/detailed-design-raw.md b/docs/detailed-design-raw.md new file mode 100644 index 0000000..046c305 --- /dev/null +++ b/docs/detailed-design-raw.md @@ -0,0 +1,76 @@ +```plantuml +!pragma teoz true + +title + Detailed design of "ResourceTracker" +end title + +actor "Client" as client + +box "Control plain" #MOTIVATION +participant "API Server" as apiserver + +box "Cloud environment" #Lavender +queue "Kafka" as kafka +participant "Kafka starter" as kafkastarter +participant "Agent" as agent +entity "Cloud provider" as cloudprovider +end box + +end box + +note over [[[[[[kafka]]]]]]: Kafka is considered to be used in persisted mode + +opt "endpoints" +opt "/v1/secrets/acquire [POST]" +apiserver -> cloudprovider: validate provided credentials +cloudprovider -> apiserver: validation result +end +opt "/v1/topic/logs [GET]" +apiserver -> kafka: retrieve state for the given "logs" topic +kafka -> apiserver: transform data stream according to the specified filters +end +opt "/v1/terraform/apply [POST]" +apiserver -> cloudprovider: deploy resource tracking infrastructure +apiserver -> kafkastarter: request kafka cluster startup +kafkastarter -> kafka: start kafka cluster +end +opt "/v1/terraform/destroy [POST]" +apiserver -> cloudprovider: destroy resource tracking infrastructure +end +end + +opt "agent execution flow" +agent -> cloudprovider: execute remote operations +agent <-- cloudprovider: remote operation result +agent --> kafka: push latest resource state to "logs" topic +end + +opt "requests" +note over client: Uses properties specified in a client\nconfiguration file located in\n a common directory +opt "credentials validation" +client -> apiserver: /v1/secrets/acquire [POST] +end +opt "script validation" +client -> apiserver: /v1/script/acquire [POST] +end +opt "health check" +client -> apiserver: /v1/health [GET] +end +opt "readiness check" +client -> apiserver: /v1/readiness [GET] +end +opt "version validation" +client -> apiserver: /v1/info/version [GET] +end +opt "infrustructure deployment" +client -> apiserver: /v1/terraform/apply [POST] +end +opt "infrustructure clean up" +client -> apiserver: /v1/terraform/destroy [POST] +end +opt "state retrieval" +client -> apiserver: /v1/topic/logs [GET] +end +end +``` \ No newline at end of file diff --git a/docs/detailed-design.png b/docs/detailed-design.png new file mode 100644 index 0000000000000000000000000000000000000000..2c30540258c070a54b733ced111a29c5eda7e185 GIT binary patch literal 124433 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�zrTe$6i%(!4@JL&3iqnGIE2Soh24j&4cJOC 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≠LnZxZ0{l4 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 z [API Server]: " Send requests" +[API Server] <-> [Cluster]: " Schedule content processing" +[Cluster] <--> [Worker]: " Propagate individual requests " +[API Server] --> [Prometheus]: " Export diagnostics" +``` \ No newline at end of file diff --git a/docs/high-level-design.png b/docs/high-level-design.png new file mode 100644 index 0000000000000000000000000000000000000000..d589fff6e54ff64ea5fb07767cc809da829c7dbe GIT binary patch literal 27497 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 literal 0 HcmV?d00001 diff --git a/exporter/pom.xml b/exporter/pom.xml new file mode 100644 index 0000000..77859cb --- /dev/null +++ b/exporter/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + com.repoachiever + base + 1.0-SNAPSHOT + + + org.example + exporter + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/exporter/src/main/java/org/example/Main.java b/exporter/src/main/java/org/example/Main.java new file mode 100644 index 0000000..407f157 --- /dev/null +++ b/exporter/src/main/java/org/example/Main.java @@ -0,0 +1,7 @@ +package org.example; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} \ No newline at end of file diff --git a/gui/.DS_Store b/gui/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1cc341eca9c6844b688eeb99b29050ad97726692 GIT binary patch literal 6148 zcmeHKO>5gg5S@)3TW$#W(3V^Rf?kd52474sisK$~tx9ufizCIx6_K^ZlIwsm=-84U zlUsjE|4#E}cOB}FUSnf+VD^o6-t4juX;(``qCM%oBdQXS17|G0K(oO(&%R~{riYV4 zjS%dxgz*~Nc31_h0=uRFf4gNG(1aqY!=Jui86v~m!a2PDR~aH^k4S=7r@Ia8 zhZS~#QX12Z-V+vMiqp-kaGH+7Sh0SGqc}>lM&pk-C=?G%WvA>MIp6&6a_VP6Hc8vT z_zORMk}?h#aS(ov`qOUp<%LYLAWHgURS@<1pnScGlAfHl + + + + + + + \ No newline at end of file diff --git a/gui/pom.xml b/gui/pom.xml new file mode 100644 index 0000000..025ae62 --- /dev/null +++ b/gui/pom.xml @@ -0,0 +1,206 @@ + + + 4.0.0 + gui + 1.0-SNAPSHOT + gui + GUI for ResourceTracker + + + com.repoachiever + base + 1.0-SNAPSHOT + + + + + com.repoachiever.GUI + + + + + + + Shell-Command-Executor-Lib + Shell-Command-Executor-Lib + system + ${basedir}/../lib/Shell-Command-Executor-Lib-0.5.0-SNAPSHOT.jar + + + ink.bluecloud + elementfx + system + ${basedir}/../lib/ElementFX-1.3-SNAPSHOT.jar + + + + + org.openjfx + javafx-controls + + + com.brunomnsilva + smartgraph + + + + + org.jetbrains.kotlin + kotlin-stdlib + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework + spring-context + + + + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 + + + + + org.openapitools + jackson-databind-nullable + + + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.projectlombok + lombok + + + org.yaml + snakeyaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.opencsv + opencsv + + + commons-io + commons-io + + + io.github.kostaskougios + cloning + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + + gui + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.openjfx + javafx-maven-plugin + + + org.openapitools + openapi-generator-maven-plugin + + + + generate + + + ${project.basedir}/../api-server/src/main/openapi/openapi.yml + java + webclient + ${default.package}.api + ${default.package}.model + true + false + false + false + false + false + true + false + + @lombok.Data @lombok.AllArgsConstructor(staticName = "of") + src/main/java + true + false + true + true + false + true + java8 + true + true + + + + + + + pl.project13.maven + git-commit-id-plugin + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + + + + ${basedir}/target/gui.jar + ${main.basedir}/../bin/gui/gui.jar + + + + + + + diff --git a/gui/src/.DS_Store b/gui/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e77e13bdf44af63bb83fb276625183d921a3f2e7 GIT binary patch literal 6148 zcmeHK!AiqG5Z!H~O(;SRDjow~i$>|Cc!{wdyc*GiN=;1BV49UCHHT8jQGdu!@pp7) zcT>>nNf0SBF#9I6GaKe@*v&A;xIc;a7_%8;CMaUVg5ev%dDI1IXipQ6;~FQaxQ$W~ z!80yM^8XpYy))Q|g)Cv&m;F10xt(_@vXFUPROXL*pI@WLG|3CsedCR$v9j7STV~t5 z4(>%66k##R{cwDVPiI1;QEi9Oc`_^q_WF^?iZIEBW0{b|LkPLLNU~UzzL;cjE^`Al zVOpj&us5gE?!Mb`+*!w&_R#8eyR(^PZfx%!oDLq+Q6`=h@f^NNDQg;QcmZQx%~LSW zQjy((ua2+d2#En=fEZX#2J~rXwwJR!s)`sO27bW+o(~QvqNlM^D31;pWD5XjfLjXK z^4A~GK?k6xu~G;g5Ux@IRVvpl2G^YfSLrbC>G(>aN@rZY8P?Hn=DI`Sy7AzS3}@U^ zNFy;o418t)dp{5x@BibU`+pfk12I4hEGGlp=m&itYSVY?LX~*eI-ob8C>U2MoTq>x iuVRSBt9T1k3iur~06mSBLhyjlkAS3s24dh(8TbIZ_f$dv literal 0 HcmV?d00001 diff --git a/gui/src/main/.DS_Store b/gui/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..879ce8ecfbf7bbe8fdd0164d9dd04507985d5066 GIT binary patch literal 6148 zcmeHKO;5r=5S;~-HpGO38jnr95=Fy>c&Ug7uh!^64YokETDyi)I1rK^_1Adwr}%et zW_K~+!+29-W|G-A`|-Bj*JgJI07N70>;aSkz(Oa?*;p(v>L;JGoM|Z_3Yz02lIA;qXo@eJejXGSngp3Fb;7Cc2^ z(yvzEMJ``hSuI*ct7Kh!cXH_U{eGG>{N5$i&ZLZjN$dybVRzUnuOG>{?}u@>rwC!E zi$kt1!nh-c4VlKBL~%XcVcC}5DsPTPwf$gk51%6e5Pi;zO Ar~m)} literal 0 HcmV?d00001 diff --git a/gui/src/main/java/com/repoachiever/App.java b/gui/src/main/java/com/repoachiever/App.java new file mode 100644 index 0000000..668ba82 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/App.java @@ -0,0 +1,81 @@ +package com.repoachiever; + +import com.repoachiever.service.client.observer.ResourceObserver; +import com.repoachiever.service.element.font.FontLoader; +import com.repoachiever.service.element.observer.ElementObserver; +import com.repoachiever.service.element.stage.MainStage; +import com.repoachiever.service.event.state.LocalState; +import com.repoachiever.service.scheduler.SchedulerHelper; +import javafx.application.Application; +import javafx.application.HostServices; +import javafx.application.Platform; +import javafx.stage.Stage; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; + +/** Represents entrypoint for the application. */ +public class App extends Application { + private ConfigurableApplicationContext applicationContext; + + @Autowired private LocalState localState; + + @Autowired private ElementObserver elementObserver; + + @Autowired private ResourceObserver resourceObserver; + + @Autowired private FontLoader fontLoader; + + @Autowired private MainStage mainStage; + + /** + * @see Application + */ + public void launch() { + Application.launch(); + } + + /** + * @see Application + */ + @Override + public void init() { + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("apple.awt.UIElement", "true"); + + ApplicationContextInitializer initializer = + applicationContext -> { + applicationContext.registerBean(Application.class, () -> App.this); + applicationContext.registerBean(Parameters.class, this::getParameters); + applicationContext.registerBean(HostServices.class, this::getHostServices); + }; + + applicationContext = + new SpringApplicationBuilder() + .sources(GUI.class) + .initializers(initializer) + .run(getParameters().getRaw().toArray(new String[0])); + } + + /** + * @see Application + */ + @Override + public void stop() { + applicationContext.close(); + SchedulerHelper.close(); + Platform.exit(); + } + + /** + * @see Application + */ + @Override + @SneakyThrows + public void start(Stage stage) { + mainStage.getContent().show(); + } +} diff --git a/gui/src/main/java/com/repoachiever/GUI.java b/gui/src/main/java/com/repoachiever/GUI.java new file mode 100644 index 0000000..1bd752d --- /dev/null +++ b/gui/src/main/java/com/repoachiever/GUI.java @@ -0,0 +1,11 @@ +package com.repoachiever; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GUI { + public static void main(String[] args) { + App app = new App(); + app.launch(); + } +} diff --git a/gui/src/main/java/com/repoachiever/converter/CredentialsConverter.java b/gui/src/main/java/com/repoachiever/converter/CredentialsConverter.java new file mode 100644 index 0000000..e6abb88 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/converter/CredentialsConverter.java @@ -0,0 +1,11 @@ +package com.repoachiever.converter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class CredentialsConverter { + @SuppressWarnings("unchecked") + public static T convert(Object input, Class stub) { + ObjectMapper mapper = new ObjectMapper(); + return mapper.convertValue(input, stub); + } +} diff --git a/gui/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java b/gui/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java new file mode 100644 index 0000000..0051755 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/CommandExecutorOutputDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents gathered output of the executed command. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class CommandExecutorOutputDto { + private String normalOutput; + + private String errorOutput; +} diff --git a/gui/src/main/java/com/repoachiever/dto/HealthCheckInternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/HealthCheckInternalCommandResultDto.java new file mode 100644 index 0000000..122fda8 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/HealthCheckInternalCommandResultDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents readiness check command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class HealthCheckInternalCommandResultDto { + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/dto/ReadinessCheckInternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/ReadinessCheckInternalCommandResultDto.java new file mode 100644 index 0000000..159ed89 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/ReadinessCheckInternalCommandResultDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents readiness check command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ReadinessCheckInternalCommandResultDto { + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/dto/StartExternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/StartExternalCommandResultDto.java new file mode 100644 index 0000000..8030ae6 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/StartExternalCommandResultDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents start deployment command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class StartExternalCommandResultDto { + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/dto/StateExternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/StateExternalCommandResultDto.java new file mode 100644 index 0000000..927de4d --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/StateExternalCommandResultDto.java @@ -0,0 +1,16 @@ +package com.repoachiever.dto; + +import com.repoachiever.model.TopicLogsResult; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents state command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class StateExternalCommandResultDto { + private TopicLogsResult topicLogsResult; + + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/dto/StopExternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/StopExternalCommandResultDto.java new file mode 100644 index 0000000..881b7fe --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/StopExternalCommandResultDto.java @@ -0,0 +1,13 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents stop deployment command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class StopExternalCommandResultDto { + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java b/gui/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java new file mode 100644 index 0000000..1749e85 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/ValidationScriptApplicationDto.java @@ -0,0 +1,12 @@ +package com.repoachiever.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents script validation application used for script acquiring process. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ValidationScriptApplicationDto { + private List fileContent; +} diff --git a/gui/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java b/gui/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java new file mode 100644 index 0000000..288b23c --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/ValidationSecretsApplicationDto.java @@ -0,0 +1,14 @@ +package com.repoachiever.dto; + +import com.repoachiever.model.Provider; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents secrets validation application used for secrets acquiring process. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class ValidationSecretsApplicationDto { + private Provider provider; + + private String filePath; +} diff --git a/gui/src/main/java/com/repoachiever/dto/VersionExternalCommandResultDto.java b/gui/src/main/java/com/repoachiever/dto/VersionExternalCommandResultDto.java new file mode 100644 index 0000000..622e8db --- /dev/null +++ b/gui/src/main/java/com/repoachiever/dto/VersionExternalCommandResultDto.java @@ -0,0 +1,15 @@ +package com.repoachiever.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Represents version retrieval command result. */ +@Getter +@AllArgsConstructor(staticName = "of") +public class VersionExternalCommandResultDto { + private String data; + + private Boolean status; + + private String error; +} diff --git a/gui/src/main/java/com/repoachiever/entity/ConfigEntity.java b/gui/src/main/java/com/repoachiever/entity/ConfigEntity.java new file mode 100644 index 0000000..c88e21d --- /dev/null +++ b/gui/src/main/java/com/repoachiever/entity/ConfigEntity.java @@ -0,0 +1,66 @@ +package com.repoachiever.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import java.util.List; +import lombok.Getter; + +/** Represents configuration model used for ResourceTracker deployment operation. */ +@Getter +public class ConfigEntity { + /** Represents request to be executed in remote environment. */ + @Getter + public static class Request { + @NotBlank String name; + + @Pattern(regexp = "^(((./)?)|((~/.)?)|((/?))?)([a-zA-Z/]*)((\\.([a-z]+))?)$") + String file; + + @Pattern( + regexp = + "(((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?)|(([\\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9]))|([\\?])|([\\*]))[\\s](((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?)|(([\\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3]))|([\\?])|([\\*]))[\\s](((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?)|(([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?)|(L(-[0-9])?)|(L(-[1-2][0-9])?)|(L(-[3][0-1])?)|(LW)|([1-9]W)|([1-3][0-9]W)|([\\?])|([\\*]))[\\s](((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?)|(([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2]))|(((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)|((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))|([\\?])|([\\*]))[\\s]((([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?)|([1-7]/([1-7]))|(((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?)|((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?)|(([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))?(L|LW)?)|(([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?)|([\\?])|([\\*]))([\\s]?(([\\*])?|(19[7-9][0-9])|(20[0-9][0-9]))?|" + + " (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))?|" + + " ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)?)") + String frequency; + } + + List requests; + + /** + * Represents remove cloud infrastructure configuration properties used for further deployment + * related operations. + */ + @Getter + public static class Cloud { + @JsonFormat(shape = JsonFormat.Shape.OBJECT) + public enum Provider { + @JsonProperty("aws") + AWS, + } + + @NotBlank Provider provider; + + @Getter + public static class AWSCredentials { + @Pattern(regexp = "^(((./)?)|((~/.)?)|((/?))?)([a-zA-Z/]*)((\\.([a-z]+))?)$") + String file; + + @NotBlank String region; + } + + @NotBlank Object credentials; + } + + Cloud cloud; + + /** Represents API Server configuration used for further connection establishment. */ + @Getter + public static class APIServer { + @NotBlank String host; + } + + @JsonProperty("api-server") + APIServer apiServer; +} diff --git a/gui/src/main/java/com/repoachiever/entity/PropertiesEntity.java b/gui/src/main/java/com/repoachiever/entity/PropertiesEntity.java new file mode 100644 index 0000000..6e35d20 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/entity/PropertiesEntity.java @@ -0,0 +1,211 @@ +package com.repoachiever.entity; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.io.ClassPathResource; + +/** Represents application properties used for application configuration. */ +@Getter +@Configuration +public class PropertiesEntity { + private static final String GIT_CONFIG_PROPERTIES_FILE = "git.properties"; + + @Value(value = "${window.main.name}") + private String windowMainName; + + @Value(value = "${window.main.scale.min.width}") + private Double windowMainScaleMinWidth; + + @Value(value = "${window.main.scale.min.height}") + private Double windowMainScaleMinHeight; + + @Value(value = "${window.main.scale.max.width}") + private Double windowMainScaleMaxWidth; + + @Value(value = "${window.main.scale.max.height}") + private Double windowMainScaleMaxHeight; + + @Value(value = "${window.settings.name}") + private String windowSettingsName; + + @Value(value = "${window.settings.scale.width}") + private Double windowSettingsScaleWidth; + + @Value(value = "${window.settings.scale.height}") + private Double windowSettingsScaleHeight; + + @Value(value = "${process.background.period}") + private Integer processBackgroundPeriod; + + @Value(value = "${process.healthcheck.period}") + private Integer processHealthcheckPeriod; + + @Value(value = "${process.readiness.period}") + private Integer processReadinessPeriod; + + @Value(value = "${process.window.width.period}") + private Integer processWindowWidthPeriod; + + @Value(value = "${process.window.height.period}") + private Integer processWindowHeightPeriod; + + @Value(value = "${spinner.initial.delay}") + private Integer spinnerInitialDelay; + + @Value(value = "${button.basic.size.width}") + private Double basicButtonSizeWidth; + + @Value(value = "${button.basic.size.height}") + private Double basicButtonSizeHeight; + + @Value(value = "${scene.general.background.color.r}") + private Integer generalBackgroundColorR; + + @Value(value = "${scene.general.background.color.g}") + private Integer generalBackgroundColorG; + + @Value(value = "${scene.general.background.color.b}") + private Integer generalBackgroundColorB; + + @Value(value = "${scene.common.header.background.color.r}") + private Integer commonSceneHeaderBackgroundColorR; + + @Value(value = "${scene.common.header.background.color.g}") + private Integer commonSceneHeaderBackgroundColorG; + + @Value(value = "${scene.common.header.background.color.b}") + private Integer commonSceneHeaderBackgroundColorB; + + @Value(value = "${scene.common.header.connection.background.color.r}") + private Integer commonSceneHeaderConnectionStatusBackgroundColorR; + + @Value(value = "${scene.common.header.connection.background.color.g}") + private Integer commonSceneHeaderConnectionStatusBackgroundColorG; + + @Value(value = "${scene.common.header.connection.background.color.b}") + private Integer commonSceneHeaderConnectionStatusBackgroundColorB; + + @Value(value = "${scene.common.menu.background.color.r}") + private Integer commonSceneMenuBackgroundColorR; + + @Value(value = "${scene.common.menu.background.color.g}") + private Integer commonSceneMenuBackgroundColorG; + + @Value(value = "${scene.common.menu.background.color.b}") + private Integer commonSceneMenuBackgroundColorB; + + @Value(value = "${scene.common.content.background.color.r}") + private Integer commonSceneContentBackgroundColorR; + + @Value(value = "${scene.common.content.background.color.g}") + private Integer commonSceneContentBackgroundColorG; + + @Value(value = "${scene.common.content.background.color.b}") + private Integer commonSceneContentBackgroundColorB; + + @Value(value = "${scene.common.content.vertical-gap}") + private Double commonSceneContentVerticalGap; + + @Value(value = "${scene.common.content.bar.horizontal-gap}") + private Double sceneCommonContentBarHorizontalGap; + + @Value(value = "${scene.common.footer.background.color.r}") + private Integer commonSceneFooterBackgroundColorR; + + @Value(value = "${scene.common.footer.background.color.g}") + private Integer commonSceneFooterBackgroundColorG; + + @Value(value = "${scene.common.footer.background.color.b}") + private Integer commonSceneFooterBackgroundColorB; + + @Value(value = "${image.status.scale}") + private Double statusImageScale; + + @Value(value = "${font.default.name}") + private String fontDefaultName; + + @Value(value = "${image.icon.name}") + private String imageIconName; + + @Value(value = "${image.arrow.name}") + private String imageArrowName; + + @Value(value = "${image.edit.name}") + private String imageEditName; + + @Value(value = "${image.refresh.name}") + private String imageRefreshName; + + @Value(value = "${image.start.name}") + private String imageStartName; + + @Value(value = "${image.stop.name}") + private String imageStopName; + + @Value(value = "${image.bar.width}") + private Integer imageBarWidth; + + @Value(value = "${image.bar.height}") + private Integer imageBarHeight; + + @Value(value = "${list-view.stub.name}") + private String listViewStubName; + + @Value(value = "${alert.api-server-unavailable.message}") + private String alertApiServerUnavailableMessage; + + @Value(value = "${alert.deployment-finished.message}") + private String alertDeploymentFinishedMessage; + + @Value(value = "${alert.destruction-finished.message}") + private String alertDestructionFinishedMessage; + + @Value(value = "${alert.version-mismatch.message}") + private String alertVersionMismatchMessage; + + @Value(value = "${alert.editor-close-reminder.message}") + private String alertEditorCloseReminderMessage; + + @Value(value = "${graph.css.location}") + private String graphCssFileLocation; + + @Value(value = "${graph.properties.location}") + private String graphPropertiesLocation; + + @Value(value = "${config.root}") + private String configRootPath; + + @Value(value = "${swap.root}") + private String swapRootPath; + + @Value(value = "${config.user.file}") + private String configUserFilePath; + + @Value(value = "${api-server.directory}") + private String apiServerDirectory; + + @Value(value = "${git.commit.id.abbrev}") + private String gitCommitId; + + @Bean + private static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { + PropertySourcesPlaceholderConfigurer propsConfig = new PropertySourcesPlaceholderConfigurer(); + propsConfig.setLocation(new ClassPathResource(GIT_CONFIG_PROPERTIES_FILE)); + propsConfig.setIgnoreResourceNotFound(true); + propsConfig.setIgnoreUnresolvablePlaceholders(true); + return propsConfig; + } + + /** + * Removes the last symbol in git commit id of the repository. + * + * @return chopped repository git commit id. + */ + public String getGitCommitId() { + return StringUtils.chop(gitCommitId); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/ApiServerException.java b/gui/src/main/java/com/repoachiever/exception/ApiServerException.java new file mode 100644 index 0000000..d89b655 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/ApiServerException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ApiServerException extends IOException { + public ApiServerException() { + this(""); + } + + public ApiServerException(Object... message) { + super( + new Formatter() + .format("API Server exception: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java b/gui/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java new file mode 100644 index 0000000..f8d332a --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/ApiServerNotAvailableException.java @@ -0,0 +1,10 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Formatter; + +public class ApiServerNotAvailableException extends IOException { + public ApiServerNotAvailableException(Object... message) { + super(new Formatter().format("API Server is not available", message).toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/ApplicationFontFileNotFoundException.java b/gui/src/main/java/com/repoachiever/exception/ApplicationFontFileNotFoundException.java new file mode 100644 index 0000000..77e759c --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/ApplicationFontFileNotFoundException.java @@ -0,0 +1,16 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ApplicationFontFileNotFoundException extends IOException { + public ApplicationFontFileNotFoundException(Object... message) { + super( + new Formatter() + .format( + "Application font file at the given location is not available: %s", + Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/ApplicationImageFileNotFoundException.java b/gui/src/main/java/com/repoachiever/exception/ApplicationImageFileNotFoundException.java new file mode 100644 index 0000000..ec1ed6c --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/ApplicationImageFileNotFoundException.java @@ -0,0 +1,20 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class ApplicationImageFileNotFoundException extends IOException { + public ApplicationImageFileNotFoundException() { + this(""); + } + + public ApplicationImageFileNotFoundException(Object... message) { + super( + new Formatter() + .format( + "Application image file at the given location is not available: %s", + Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java b/gui/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java new file mode 100644 index 0000000..43961c6 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/CloudCredentialsFileNotFoundException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class CloudCredentialsFileNotFoundException extends IOException { + public CloudCredentialsFileNotFoundException() { + this(""); + } + + public CloudCredentialsFileNotFoundException(Object... message) { + super( + new Formatter() + .format( + "Given cloud credentials file is not found: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java b/gui/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java new file mode 100644 index 0000000..713b34f --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/CloudCredentialsValidationException.java @@ -0,0 +1,18 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +public class CloudCredentialsValidationException extends IOException { + public CloudCredentialsValidationException() { + this(""); + } + + public CloudCredentialsValidationException(Object... message) { + super( + new Formatter() + .format("Given cloud credentials are not valid!: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/CommandExecutorException.java b/gui/src/main/java/com/repoachiever/exception/CommandExecutorException.java new file mode 100644 index 0000000..600bcb3 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/CommandExecutorException.java @@ -0,0 +1,10 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Formatter; + +public class CommandExecutorException extends IOException { + public CommandExecutorException(Object... message) { + super(new Formatter().format("Invalid command executor behaviour", message).toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java b/gui/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java new file mode 100644 index 0000000..17250fb --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/ScriptDataValidationException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** Represents exception, when given file is not valid. */ +public class ScriptDataValidationException extends IOException { + public ScriptDataValidationException() { + this(""); + } + + public ScriptDataValidationException(Object... message) { + super( + new Formatter() + .format("Given explicit script file is not valid: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/SmartGraphCssFileNotFoundException.java b/gui/src/main/java/com/repoachiever/exception/SmartGraphCssFileNotFoundException.java new file mode 100644 index 0000000..856d7e8 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/SmartGraphCssFileNotFoundException.java @@ -0,0 +1,13 @@ +package com.repoachiever.exception; + +import java.io.FileNotFoundException; +import java.util.Formatter; + +public class SmartGraphCssFileNotFoundException extends FileNotFoundException { + public SmartGraphCssFileNotFoundException(Object... message) { + super( + new Formatter() + .format("SmartGraph CSS file at the given location is not available", message) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/SmartGraphPropertiesFileNotFoundException.java b/gui/src/main/java/com/repoachiever/exception/SmartGraphPropertiesFileNotFoundException.java new file mode 100644 index 0000000..5701c9f --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/SmartGraphPropertiesFileNotFoundException.java @@ -0,0 +1,13 @@ +package com.repoachiever.exception; + +import java.io.FileNotFoundException; +import java.util.Formatter; + +public class SmartGraphPropertiesFileNotFoundException extends FileNotFoundException { + public SmartGraphPropertiesFileNotFoundException(Object... message) { + super( + new Formatter() + .format("SmartGraph properties file at the given location is not available", message) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/SwapFileCreationFailedException.java b/gui/src/main/java/com/repoachiever/exception/SwapFileCreationFailedException.java new file mode 100644 index 0000000..c14abf4 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/SwapFileCreationFailedException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** Represents exception, when swap file creation failed. */ +public class SwapFileCreationFailedException extends IOException { + public SwapFileCreationFailedException() { + this(""); + } + + public SwapFileCreationFailedException(Object... message) { + super( + new Formatter() + .format("Swap file creation failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/exception/SwapFileDeletionFailedException.java b/gui/src/main/java/com/repoachiever/exception/SwapFileDeletionFailedException.java new file mode 100644 index 0000000..5f4a5d1 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/exception/SwapFileDeletionFailedException.java @@ -0,0 +1,19 @@ +package com.repoachiever.exception; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Formatter; + +/** Represents exception, when swap file deletion failed. */ +public class SwapFileDeletionFailedException extends IOException { + public SwapFileDeletionFailedException() { + this(""); + } + + public SwapFileDeletionFailedException(Object... message) { + super( + new Formatter() + .format("Swap file deletion failed: %s", Arrays.stream(message).toArray()) + .toString()); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java new file mode 100644 index 0000000..274c3c6 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/ApplyClientCommandService.java @@ -0,0 +1,52 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TerraformResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TerraformDeploymentApplication; +import com.repoachiever.model.TerraformDeploymentApplicationResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents apply client command service. */ +@Service +public class ApplyClientCommandService + implements IClientCommand< + TerraformDeploymentApplicationResult, TerraformDeploymentApplication> { + @Autowired private ConfigService configService; + + private TerraformResourceApi terraformResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.terraformResourceApi = new TerraformResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public TerraformDeploymentApplicationResult process(TerraformDeploymentApplication input) + throws ApiServerException { + try { + return terraformResourceApi.v1TerraformApplyPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java new file mode 100644 index 0000000..fa4ffd1 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/DestroyClientCommandService.java @@ -0,0 +1,47 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TerraformResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TerraformDestructionApplication; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents destroy client command service. */ +@Service +public class DestroyClientCommandService + implements IClientCommand { + @Autowired private ConfigService configService; + private TerraformResourceApi terraformResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.terraformResourceApi = new TerraformResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public Void process(TerraformDestructionApplication input) throws ApiServerException { + try { + return terraformResourceApi.v1TerraformDestroyPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java new file mode 100644 index 0000000..9a7162f --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/HealthCheckClientCommandService.java @@ -0,0 +1,47 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.HealthResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents health check client command service. */ +@Service +public class HealthCheckClientCommandService implements IClientCommand { + @Autowired private ConfigService configService; + + private HealthResourceApi healthResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.healthResourceApi = new HealthResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public HealthCheckResult process(Void input) throws ApiServerException { + try { + return healthResourceApi.v1HealthGet().block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java new file mode 100644 index 0000000..6ff0026 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/LogsClientCommandService.java @@ -0,0 +1,46 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.TopicResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.TopicLogsApplication; +import com.repoachiever.model.TopicLogsResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents logs topic client command service. */ +@Service +public class LogsClientCommandService + implements IClientCommand { + private final TopicResourceApi topicResourceApi; + + public LogsClientCommandService(@Autowired ConfigService configService) { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.topicResourceApi = new TopicResourceApi(apiClient); + } + + /** */ + @Override + public void configure() {} + + /** + * @see IClientCommand + */ + @Override + public TopicLogsResult process(TopicLogsApplication input) throws ApiServerException { + try { + return topicResourceApi.v1TopicLogsPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java new file mode 100644 index 0000000..75a3b9f --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/ReadinessCheckClientCommandService.java @@ -0,0 +1,49 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.HealthResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ReadinessCheckApplication; +import com.repoachiever.model.ReadinessCheckResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents readiness check client command service. */ +@Service +public class ReadinessCheckClientCommandService + implements IClientCommand { + @Autowired private ConfigService configService; + + private HealthResourceApi healthResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.healthResourceApi = new HealthResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public ReadinessCheckResult process(ReadinessCheckApplication input) throws ApiServerException { + try { + return healthResourceApi.v1ReadinessPost(input).block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java new file mode 100644 index 0000000..d2c089c --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/ScriptAcquireClientCommandService.java @@ -0,0 +1,54 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.ValidationResourceApi; +import com.repoachiever.dto.ValidationScriptApplicationDto; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ValidationScriptApplication; +import com.repoachiever.model.ValidationScriptApplicationResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents script validation client command service. */ +@Service +public class ScriptAcquireClientCommandService + implements IClientCommand { + @Autowired private ConfigService configService; + + private ValidationResourceApi validationResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.validationResourceApi = new ValidationResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public ValidationScriptApplicationResult process(ValidationScriptApplicationDto input) + throws ApiServerException { + try { + return validationResourceApi + .v1ScriptAcquirePost(ValidationScriptApplication.of(input.getFileContent())) + .block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java new file mode 100644 index 0000000..f3d9d2a --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/SecretsAcquireClientCommandService.java @@ -0,0 +1,74 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.ValidationResourceApi; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.exception.CloudCredentialsFileNotFoundException; +import com.repoachiever.model.Provider; +import com.repoachiever.model.ValidationSecretsApplication; +import com.repoachiever.model.ValidationSecretsApplicationResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents secrets validation client command service. */ +@Service +public class SecretsAcquireClientCommandService + implements IClientCommand { + @Autowired private ConfigService configService; + + private ValidationResourceApi validationResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.validationResourceApi = new ValidationResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + @Override + public ValidationSecretsApplicationResult process(ValidationSecretsApplicationDto input) + throws ApiServerException { + Path filePath = Paths.get(input.getFilePath()); + + if (Files.notExists(filePath)) { + throw new ApiServerException(new CloudCredentialsFileNotFoundException().getMessage()); + } + + String content; + + try { + content = Files.readString(filePath); + } catch (IOException e) { + throw new ApiServerException(e.getMessage()); + } + + try { + return validationResourceApi + .v1SecretsAcquirePost(ValidationSecretsApplication.of(Provider.AWS, content)) + .block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getHeaders()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java b/gui/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java new file mode 100644 index 0000000..b53a972 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/command/VersionClientCommandService.java @@ -0,0 +1,47 @@ +package com.repoachiever.service.client.command; + +import com.repoachiever.ApiClient; +import com.repoachiever.api.InfoResourceApi; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.ApplicationInfoResult; +import com.repoachiever.service.client.common.IClientCommand; +import com.repoachiever.service.config.ConfigService; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +/** Represents version information client command service. */ +@Service +public class VersionClientCommandService implements IClientCommand { + @Autowired private ConfigService configService; + + private InfoResourceApi infoResourceApi; + + /** + * @see IClientCommand + */ + @Override + @PostConstruct + public void configure() { + ApiClient apiClient = + new ApiClient().setBasePath(configService.getConfig().getApiServer().getHost()); + + this.infoResourceApi = new InfoResourceApi(apiClient); + } + + /** + * @see IClientCommand + */ + public ApplicationInfoResult process(Void input) throws ApiServerException { + try { + return infoResourceApi.v1InfoVersionGet().block(); + } catch (WebClientResponseException e) { + throw new ApiServerException(e.getResponseBodyAsString()); + } catch (WebClientRequestException e) { + throw new ApiServerException(new ApiServerNotAvailableException(e.getMessage()).getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/client/common/IClientCommand.java b/gui/src/main/java/com/repoachiever/service/client/common/IClientCommand.java new file mode 100644 index 0000000..c6645e6 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/common/IClientCommand.java @@ -0,0 +1,22 @@ +package com.repoachiever.service.client.common; + +import com.repoachiever.exception.ApiServerException; + +/** + * Represents external resource command interface. + * + * @param type of the command response. + * @param type of the command request. + */ +public interface IClientCommand { + /** Provides configuration of the API Client. */ + void configure(); + + /** + * Processes certain request for an external command. + * + * @param input input to be given as request body. + * @return command response. + */ + T process(K input) throws ApiServerException; +} diff --git a/gui/src/main/java/com/repoachiever/service/client/observer/ResourceObserver.java b/gui/src/main/java/com/repoachiever/service/client/observer/ResourceObserver.java new file mode 100644 index 0000000..91fa45f --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/client/observer/ResourceObserver.java @@ -0,0 +1,52 @@ +package com.repoachiever.service.client.observer; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.service.client.command.HealthCheckClientCommandService; +import com.repoachiever.service.client.command.ReadinessCheckClientCommandService; +import com.repoachiever.service.event.payload.ConnectionStatusEvent; +import com.repoachiever.service.hand.executor.CommandExecutorService; +import com.repoachiever.service.scheduler.SchedulerHelper; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +/** Provides resource observables to manage state of the application. */ +@Component +public class ResourceObserver { + @Autowired private ApplicationEventPublisher applicationEventPublisher; + + @Autowired private PropertiesEntity properties; + + @Autowired private CommandExecutorService commandExecutorService; + + @Autowired private HealthCheckClientCommandService healthCommandService; + + @Autowired private ReadinessCheckClientCommandService readinessCommandService; + + /** Sends healthcheck requests to API Server and updates connection status. */ + @PostConstruct + private void handleHealthCommand() { + SchedulerHelper.scheduleTask( + () -> { + ConnectionStatusEvent connectionStatusEvent; + + try { + HealthCheckResult result = healthCommandService.process(null); + + connectionStatusEvent = + switch (result.getStatus()) { + case UP -> new ConnectionStatusEvent(true); + case DOWN -> new ConnectionStatusEvent(false); + }; + } catch (ApiServerException e) { + connectionStatusEvent = new ConnectionStatusEvent(false); + } + + applicationEventPublisher.publishEvent(connectionStatusEvent); + }, + properties.getProcessHealthcheckPeriod()); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/common/ICommand.java b/gui/src/main/java/com/repoachiever/service/command/common/ICommand.java new file mode 100644 index 0000000..1352379 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/common/ICommand.java @@ -0,0 +1,9 @@ +package com.repoachiever.service.command.common; + +import com.repoachiever.exception.ApiServerException; + +/** Represents common command interface. */ +public interface ICommand { + /** Processes certain request for an external command. */ + T process() throws ApiServerException; +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java new file mode 100644 index 0000000..e6096db --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/start/StartExternalCommandService.java @@ -0,0 +1,25 @@ +package com.repoachiever.service.command.external.start; + +import com.repoachiever.dto.StartExternalCommandResultDto; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.start.provider.aws.AWSStartExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service. */ +@Service +public class StartExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStartExternalCommandService awsStartExternalCommandService; + + /** + * @see ICommand + */ + public StartExternalCommandResultDto process() { + return switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsStartExternalCommandService.process(); + }; + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java new file mode 100644 index 0000000..faadc82 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/start/provider/aws/AWSStartExternalCommandService.java @@ -0,0 +1,110 @@ +package com.repoachiever.service.command.external.start.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.StartExternalCommandResultDto; +import com.repoachiever.dto.ValidationScriptApplicationDto; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.exception.ScriptDataValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.ApplyClientCommandService; +import com.repoachiever.service.client.command.ScriptAcquireClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service for AWS provider. */ +@Service +public class AWSStartExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private ApplyClientCommandService applyClientCommandService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private ScriptAcquireClientCommandService scriptAcquireClientCommandService; + + /** + * @see ICommand + */ + @Override + public StartExternalCommandResultDto process() { + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult; + try { + validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + } catch (ApiServerException e) { + return StartExternalCommandResultDto.of(false, e.getMessage()); + } + + if (validationSecretsApplicationResult.getValid()) { + List requests = new ArrayList<>(); + + for (ConfigEntity.Request request : configService.getConfig().getRequests()) { + try { + requests.add(DeploymentRequest.of( + request.getName(), + Files.readString(Paths.get(request.getFile())), + request.getFrequency())); + } catch (IOException e) { + return StartExternalCommandResultDto.of(false, new ScriptDataValidationException(e.getMessage()).getMessage()); + } + } + + ValidationScriptApplicationDto validationScriptApplicationDto = + ValidationScriptApplicationDto.of( + requests.stream().map(DeploymentRequest::getScript).toList()); + + ValidationScriptApplicationResult validationScriptApplicationResult; + try { + validationScriptApplicationResult = + scriptAcquireClientCommandService.process(validationScriptApplicationDto); + } catch (ApiServerException e) { + return StartExternalCommandResultDto.of(false, e.getMessage()); + } + + if (validationScriptApplicationResult.getValid()) { + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TerraformDeploymentApplication terraformDeploymentApplication = + TerraformDeploymentApplication.of(requests, Provider.AWS, credentialsFields); + + try { + applyClientCommandService.process(terraformDeploymentApplication); + } catch (ApiServerException e) { + return StartExternalCommandResultDto.of(false, e.getMessage()); + } + + return StartExternalCommandResultDto.of(true, null); + } else { + return StartExternalCommandResultDto.of( + false, new ScriptDataValidationException().getMessage()); + } + } else { + return StartExternalCommandResultDto.of( + false, new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java new file mode 100644 index 0000000..a7d0d5e --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/state/StateExternalCommandService.java @@ -0,0 +1,25 @@ +package com.repoachiever.service.command.external.state; + +import com.repoachiever.dto.StateExternalCommandResultDto; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.state.provider.aws.AWSStateExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents state external command service. */ +@Service +public class StateExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStateExternalCommandService awsStateExternalCommandService; + + /** + * @see ICommand + */ + public StateExternalCommandResultDto process() { + return switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsStateExternalCommandService.process(); + }; + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java new file mode 100644 index 0000000..838a1c2 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/state/provider/aws/AWSStateExternalCommandService.java @@ -0,0 +1,72 @@ +package com.repoachiever.service.command.external.state.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.StateExternalCommandResultDto; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.LogsClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents start external command service for AWS provider. */ +@Service +public class AWSStateExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private LogsClientCommandService logsClientCommandService; + + /** + * @see ICommand + */ + @Override + public StateExternalCommandResultDto process() { + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult; + try { + validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + } catch (ApiServerException e) { + return StateExternalCommandResultDto.of(null, false, e.getMessage()); + } + + if (validationSecretsApplicationResult.getValid()) { + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TopicLogsResult topicLogsResult; + + TopicLogsApplication topicLogsApplication = + TopicLogsApplication.of(Provider.AWS, credentialsFields); + + try { + topicLogsResult = logsClientCommandService.process(topicLogsApplication); + } catch (ApiServerException e) { + return StateExternalCommandResultDto.of(null, false, e.getMessage()); + } + + return StateExternalCommandResultDto.of(topicLogsResult, true, null); + } else { + return StateExternalCommandResultDto.of( + null, false, new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java new file mode 100644 index 0000000..a6e0d83 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/stop/StopExternalCommandService.java @@ -0,0 +1,25 @@ +package com.repoachiever.service.command.external.stop; + +import com.repoachiever.dto.StopExternalCommandResultDto; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.external.stop.provider.aws.AWSStopExternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents stop external command service. */ +@Service +public class StopExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private AWSStopExternalCommandService stopExternalCommandService; + + /** + * @see ICommand + */ + public StopExternalCommandResultDto process() { + return switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> stopExternalCommandService.process(); + }; + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java new file mode 100644 index 0000000..4f58d3b --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/stop/provider/aws/AWSStopExternalCommandService.java @@ -0,0 +1,75 @@ +package com.repoachiever.service.command.external.stop.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.StopExternalCommandResultDto; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.DestroyClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** */ +@Service +public class AWSStopExternalCommandService implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private DestroyClientCommandService destroyClientCommandService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + /** + * @see ICommand + */ + @Override + public StopExternalCommandResultDto process() { + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult; + try { + validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + } catch (ApiServerException e) { + return StopExternalCommandResultDto.of(false, e.getMessage()); + } + + if (validationSecretsApplicationResult.getValid()) { + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + TerraformDestructionApplication terraformDestructionApplication = + TerraformDestructionApplication.of( + configService.getConfig().getRequests().stream() + .map(element -> DestructionRequest.of(element.getName())) + .toList(), + Provider.AWS, + credentialsFields); + + try { + destroyClientCommandService.process(terraformDestructionApplication); + } catch (ApiServerException e) { + return StopExternalCommandResultDto.of(false, e.getMessage()); + } + + return StopExternalCommandResultDto.of(true, null); + } else { + return StopExternalCommandResultDto.of( + false, new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java new file mode 100644 index 0000000..7eb4406 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/external/version/VersionExternalCommandService.java @@ -0,0 +1,33 @@ +package com.repoachiever.service.command.external.version; + +import com.repoachiever.dto.VersionExternalCommandResultDto; +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.model.ApplicationInfoResult; +import com.repoachiever.service.client.command.VersionClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents version external command service. */ +@Service +public class VersionExternalCommandService implements ICommand { + @Autowired private PropertiesEntity properties; + + @Autowired private VersionClientCommandService versionClientCommandService; + + /** + * @see ICommand + */ + public VersionExternalCommandResultDto process() { + ApplicationInfoResult applicationInfoResult; + try { + applicationInfoResult = versionClientCommandService.process(null); + } catch (ApiServerException e) { + return VersionExternalCommandResultDto.of(null, false, e.getMessage()); + } + + return VersionExternalCommandResultDto.of( + applicationInfoResult.getExternalApi().getHash(), true, null); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java new file mode 100644 index 0000000..0347b14 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/internal/health/HealthCheckInternalCommandService.java @@ -0,0 +1,41 @@ +package com.repoachiever.service.command.internal.health; + +import com.repoachiever.dto.HealthCheckInternalCommandResultDto; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.model.HealthCheckResult; +import com.repoachiever.model.HealthCheckStatus; +import com.repoachiever.service.client.command.HealthCheckClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class HealthCheckInternalCommandService + implements ICommand { + @Autowired private HealthCheckClientCommandService healthCheckClientCommandService; + + /** + * @see ICommand + */ + @Override + public HealthCheckInternalCommandResultDto process() { + HealthCheckResult healthCheckResult; + try { + healthCheckResult = healthCheckClientCommandService.process(null); + } catch (ApiServerException e) { + return HealthCheckInternalCommandResultDto.of(false, e.getMessage()); + } + + if (healthCheckResult.getStatus() == HealthCheckStatus.DOWN) { + return HealthCheckInternalCommandResultDto.of( + false, + new ApiServerException( + new ApiServerNotAvailableException(healthCheckResult.getChecks().toString()) + .getMessage()) + .getMessage()); + } + + return HealthCheckInternalCommandResultDto.of(true, null); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java new file mode 100644 index 0000000..053e8dd --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/internal/readiness/ReadinessCheckInternalCommandService.java @@ -0,0 +1,28 @@ +package com.repoachiever.service.command.internal.readiness; + +import com.repoachiever.dto.ReadinessCheckInternalCommandResultDto; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.command.internal.readiness.provider.aws.AWSReadinessCheckInternalCommandService; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents readiness check internal command service. */ +@Service +public class ReadinessCheckInternalCommandService + implements ICommand { + @Autowired private ConfigService configService; + + @Autowired + private AWSReadinessCheckInternalCommandService awsReadinessCheckInternalCommandService; + + /** + * @see ICommand + */ + @Override + public ReadinessCheckInternalCommandResultDto process() { + return switch (configService.getConfig().getCloud().getProvider()) { + case AWS -> awsReadinessCheckInternalCommandService.process(); + }; + } +} diff --git a/gui/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java b/gui/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java new file mode 100644 index 0000000..081da5c --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/command/internal/readiness/provider/aws/AWSReadinessCheckInternalCommandService.java @@ -0,0 +1,82 @@ +package com.repoachiever.service.command.internal.readiness.provider.aws; + +import com.repoachiever.converter.CredentialsConverter; +import com.repoachiever.dto.ReadinessCheckInternalCommandResultDto; +import com.repoachiever.dto.ValidationSecretsApplicationDto; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.exception.ApiServerException; +import com.repoachiever.exception.ApiServerNotAvailableException; +import com.repoachiever.exception.CloudCredentialsValidationException; +import com.repoachiever.model.*; +import com.repoachiever.service.client.command.ReadinessCheckClientCommandService; +import com.repoachiever.service.client.command.SecretsAcquireClientCommandService; +import com.repoachiever.service.command.common.ICommand; +import com.repoachiever.service.config.ConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AWSReadinessCheckInternalCommandService + implements ICommand { + @Autowired private ConfigService configService; + + @Autowired private SecretsAcquireClientCommandService secretsAcquireClientCommandService; + + @Autowired private ReadinessCheckClientCommandService readinessCheckClientCommandService; + + /** + * @see ICommand + */ + @Override + public ReadinessCheckInternalCommandResultDto process() { + ConfigEntity.Cloud.AWSCredentials credentials = + CredentialsConverter.convert( + configService.getConfig().getCloud().getCredentials(), + ConfigEntity.Cloud.AWSCredentials.class); + + ValidationSecretsApplicationDto validationSecretsApplicationDto = + ValidationSecretsApplicationDto.of(Provider.AWS, credentials.getFile()); + + ValidationSecretsApplicationResult validationSecretsApplicationResult; + try { + validationSecretsApplicationResult = + secretsAcquireClientCommandService.process(validationSecretsApplicationDto); + } catch (ApiServerException e) { + return ReadinessCheckInternalCommandResultDto.of(false, e.getMessage()); + } + + if (validationSecretsApplicationResult.getValid()) { + CredentialsFields credentialsFields = + CredentialsFields.of( + AWSSecrets.of( + validationSecretsApplicationResult.getSecrets().getAccessKey(), + validationSecretsApplicationResult.getSecrets().getSecretKey()), + credentials.getRegion()); + + ReadinessCheckApplication readinessCheckApplication = + ReadinessCheckApplication.of(Provider.AWS, credentialsFields); + + ReadinessCheckResult readinessCheckResult; + try { + readinessCheckResult = + readinessCheckClientCommandService.process(readinessCheckApplication); + } catch (ApiServerException e) { + return ReadinessCheckInternalCommandResultDto.of(false, e.getMessage()); + } + + if (readinessCheckResult.getStatus() == ReadinessCheckStatus.DOWN) { + return ReadinessCheckInternalCommandResultDto.of( + false, + new ApiServerException( + new ApiServerNotAvailableException(readinessCheckResult.getData().toString()) + .getMessage()) + .getMessage()); + } + + return ReadinessCheckInternalCommandResultDto.of(true, null); + } else { + return ReadinessCheckInternalCommandResultDto.of( + false, new CloudCredentialsValidationException().getMessage()); + } + } +} diff --git a/gui/src/main/java/com/repoachiever/service/config/ConfigService.java b/gui/src/main/java/com/repoachiever/service/config/ConfigService.java new file mode 100644 index 0000000..cb66093 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/config/ConfigService.java @@ -0,0 +1,78 @@ +package com.repoachiever.service.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.repoachiever.entity.ConfigEntity; +import com.repoachiever.entity.PropertiesEntity; +import jakarta.annotation.PostConstruct; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Represents service used for configuration processing. */ +@Service +public class ConfigService { + private static final Logger logger = LogManager.getLogger(ConfigService.class); + + @Autowired private PropertiesEntity properties; + + private ConfigEntity parsedConfigFile; + + /** Processes configuration file */ + @PostConstruct + public void configure() { + InputStream configFile; + + try { + configFile = + new FileInputStream( + Paths.get( + System.getProperty("user.home"), + properties.getConfigRootPath(), + properties.getConfigUserFilePath()) + .toString()); + } catch (FileNotFoundException e) { + logger.fatal(e.getMessage()); + return; + } + + ObjectMapper mapper = + new ObjectMapper(new YAMLFactory()) + .configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectReader reader = mapper.reader().forType(new TypeReference() {}); + + try { + parsedConfigFile = reader.readValues(configFile).readAll().getFirst(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } finally { + try { + configFile.close(); + } catch (IOException e) { + logger.fatal(e.getMessage()); + } + } + } + + /** + * Retrieves parsed configuration file entity. + * + * @return retrieved parsed configuration file entity. + */ + public ConfigEntity getConfig() { + return parsedConfigFile; + } +} diff --git a/gui/src/main/java/com/repoachiever/service/element/alert/ErrorAlert.java b/gui/src/main/java/com/repoachiever/service/element/alert/ErrorAlert.java new file mode 100644 index 0000000..f6a4d89 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/element/alert/ErrorAlert.java @@ -0,0 +1,43 @@ +package com.repoachiever.service.element.alert; + +import com.repoachiever.service.element.storage.ElementStorage; +import com.repoachiever.service.element.text.common.IElement; +import ink.bluecloud.css.CssResources; +import ink.bluecloud.css.ElementButton; +import ink.bluecloud.css.ElementButtonKt; +import java.util.UUID; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import org.springframework.stereotype.Service; + +/** Represents error related alert. */ +@Service +public class ErrorAlert implements IElement { + UUID id = UUID.randomUUID(); + + public ErrorAlert() { + Platform.runLater( + () -> { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error occurred"); + + ElementButtonKt.theme( + (Button) alert.getDialogPane().lookupButton(ButtonType.OK), ElementButton.redButton); + alert.getDialogPane().getStylesheets().add(CssResources.globalCssFile); + alert.getDialogPane().getStylesheets().add(CssResources.buttonCssFile); + alert.getDialogPane().getStylesheets().add(CssResources.textFieldCssFile); + + ElementStorage.setElement(id, alert); + }); + } + + /** + * @see IElement + */ + @Override + public Alert getContent() { + return ElementStorage.getElement(id); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/element/alert/InformationAlert.java b/gui/src/main/java/com/repoachiever/service/element/alert/InformationAlert.java new file mode 100644 index 0000000..31ca501 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/element/alert/InformationAlert.java @@ -0,0 +1,43 @@ +package com.repoachiever.service.element.alert; + +import com.repoachiever.service.element.storage.ElementStorage; +import com.repoachiever.service.element.text.common.IElement; +import ink.bluecloud.css.CssResources; +import ink.bluecloud.css.ElementButton; +import ink.bluecloud.css.ElementButtonKt; +import java.util.UUID; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import org.springframework.stereotype.Service; + +/** Represents error related alert. */ +@Service +public class InformationAlert implements IElement { + UUID id = UUID.randomUUID(); + + public InformationAlert() { + Platform.runLater( + () -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Action information"); + + ElementButtonKt.theme( + (Button) alert.getDialogPane().lookupButton(ButtonType.OK), ElementButton.redButton); + alert.getDialogPane().getStylesheets().add(CssResources.globalCssFile); + alert.getDialogPane().getStylesheets().add(CssResources.buttonCssFile); + alert.getDialogPane().getStylesheets().add(CssResources.textFieldCssFile); + + ElementStorage.setElement(id, alert); + }); + } + + /** + * @see IElement + */ + @Override + public Alert getContent() { + return ElementStorage.getElement(id); + } +} diff --git a/gui/src/main/java/com/repoachiever/service/element/button/BasicButton.java b/gui/src/main/java/com/repoachiever/service/element/button/BasicButton.java new file mode 100644 index 0000000..ddebf80 --- /dev/null +++ b/gui/src/main/java/com/repoachiever/service/element/button/BasicButton.java @@ -0,0 +1,75 @@ +package com.repoachiever.service.element.button; + +import com.repoachiever.entity.PropertiesEntity; +import com.repoachiever.service.element.common.ElementHelper; +import com.repoachiever.service.element.storage.ElementStorage; +import com.repoachiever.service.element.text.common.IElement; +import com.repoachiever.service.element.text.common.IElementResizable; +import com.repoachiever.service.event.state.LocalState; +import ink.bluecloud.css.CssResources; +import ink.bluecloud.css.ElementButton; +import ink.bluecloud.css.ElementButtonKt; +import java.util.UUID; +import javafx.geometry.Rectangle2D; +import javafx.scene.control.Button; + +/** Represents basic button. */ +public class BasicButton implements IElementResizable, IElement