├── webui ├── tests │ ├── .gitignore │ ├── Main.elm │ └── elm-package.json ├── src │ ├── Commands │ │ ├── Fetch.elm │ │ └── LoginLogout.elm │ ├── favicon-300.png │ ├── Utils │ │ ├── ListUtils.elm │ │ ├── DecodeUtils.elm │ │ ├── TaskUtils.elm │ │ ├── CmdUtils.elm │ │ ├── DictUtils.elm │ │ ├── StringUtils.elm │ │ └── HtmlUtils.elm │ ├── Models │ │ ├── Ui │ │ │ ├── Notifications.elm │ │ │ ├── LoginForm.elm │ │ │ └── InstanceParameterForm.elm │ │ └── Resources │ │ │ ├── LogKind.elm │ │ │ ├── Allocation.elm │ │ │ ├── InstanceDeleted.elm │ │ │ ├── InstanceError.elm │ │ │ ├── UserInfo.elm │ │ │ ├── PeriodicRun.elm │ │ │ ├── ServiceStatus.elm │ │ │ ├── Service.elm │ │ │ ├── InstanceUpdated.elm │ │ │ ├── TaskState.elm │ │ │ ├── InstanceCreated.elm │ │ │ ├── InstanceTasks.elm │ │ │ ├── ClientStatus.elm │ │ │ ├── Role.elm │ │ │ ├── InstanceCreation.elm │ │ │ └── JobStatus.elm │ ├── Routing.elm │ ├── Updates │ │ ├── UpdateErrors.elm │ │ └── UpdateLoginForm.elm │ ├── Views │ │ ├── Styles.elm │ │ ├── Notifications.elm │ │ ├── JobStatusView.elm │ │ └── LogUrl.elm │ ├── index.js │ └── index.css ├── .gitignore ├── README.md ├── webpack.config.prod.js └── elm-package.json ├── project ├── build.properties └── plugins.sbt ├── http-api-tests ├── broccoli-only │ ├── api-v1-about │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ └── url │ ├── root-reachable │ │ ├── method │ │ ├── url │ │ └── expected │ │ │ └── http-status │ ├── api-v1-templates-list │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ └── url │ ├── api-v1-templates-show │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ └── url │ ├── api-v1-instances-list-empty │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ └── url │ ├── api-v1-instances-show-404 │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ └── url │ ├── api-v1-templates-show-404 │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ └── url │ ├── api-v1-instances-show-after-create │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-delete │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-edit │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-parameter-id │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── request-header │ │ ├── url │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-parameters-200 │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── request-header │ │ ├── url │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-parameters-404 │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── request-header │ │ ├── url │ │ └── request-data │ ├── api-v1-instances-edit-template-200 │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ ├── request-header │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-template-notExisting │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ ├── request-header │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-parameters-notExisting │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── request-header │ │ ├── url │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-template-invalidParameters │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ ├── request-header │ │ ├── request-data │ │ └── before │ ├── api-v1-instances-edit-parameters-missing-with-default │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── request-header │ │ ├── url │ │ ├── request-data │ │ └── before │ ├── after-each │ └── before-each ├── broccoli-nomad │ ├── api-v1-instances-delete │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-start │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-stop │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-parameter-start │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-template-restart │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-parameter-restart │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-template-no-restart │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-service-status-unknown │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── after-each │ └── before-each ├── broccoli-nomad-tls │ ├── api-v1-instances-delete │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-start │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-stop │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-parameter-restart │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-parameter-start │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-template-restart │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-service-status-unknown │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-edit-template-no-restart │ │ ├── method │ │ ├── curl-options │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── after-each │ └── before-each ├── broccoli-nomad-consul │ ├── api-v1-instances-service-status │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── after-each │ ├── api-v1-examples-http-server │ │ ├── before │ │ └── run │ └── before-each ├── instance-persistence-dir │ ├── api-v1-instances-create-many │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-edit │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-create │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-delete │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── after-each │ └── before-each ├── instance-persistence-couchdb │ ├── api-v1-instances-create-many │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-create │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-delete │ │ ├── method │ │ ├── expected │ │ │ └── http-status │ │ ├── url │ │ └── before │ ├── api-v1-instances-show-after-edit │ │ ├── method │ │ ├── expected │ │ │ ├── http-status │ │ │ └── response-data │ │ ├── url │ │ └── before │ ├── after-each │ └── before-each └── common.sh ├── scalafmt ├── server └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ ├── routes │ │ └── logback.xml │ └── scala │ │ └── de │ │ └── frosner │ │ └── broccoli │ │ ├── services │ │ ├── TemplateNotFoundException.scala │ │ ├── PrefixViolationException.scala │ │ ├── NomadRequestFailed.scala │ │ ├── InvalidWebsocketConnectionException.scala │ │ ├── TemplateService.scala │ │ ├── ParameterValueExceptions.scala │ │ └── AboutInfoService.scala │ │ ├── instances │ │ ├── InstanceNotFoundException.scala │ │ ├── PeriodicJobNotFoundException.scala │ │ ├── InstanceConfiguration.scala │ │ ├── storage │ │ │ ├── filesystem │ │ │ │ ├── FileSystemConfiguration.scala │ │ │ │ └── FileSystemStorageModule.scala │ │ │ ├── couchdb │ │ │ │ └── CouchDBConfiguration.scala │ │ │ └── StorageConfiguration.scala │ │ └── InstanceModule.scala │ │ ├── conf │ │ ├── IllegalConfigException.scala │ │ └── package.scala │ │ ├── controllers │ │ ├── StatusController.scala │ │ └── AboutController.scala │ │ ├── models │ │ ├── Refresh.scala │ │ ├── InstanceCreation.scala │ │ ├── PeriodicRun.scala │ │ ├── Service.scala │ │ ├── TemplateFormat.scala │ │ ├── ServiceStatus.scala │ │ ├── JobStatus.scala │ │ ├── InstanceUpdate.scala │ │ ├── InstanceTasks.scala │ │ ├── InstanceDeleted.scala │ │ ├── InstanceUpdated.scala │ │ └── InstanceCreated.scala │ │ ├── nomad │ │ ├── models │ │ │ ├── WithId.scala │ │ │ ├── TaskLog.scala │ │ │ ├── TaskStateEvents.scala │ │ │ ├── Service.scala │ │ │ ├── TaskGroup.scala │ │ │ ├── NomadError.scala │ │ │ ├── Job.scala │ │ │ ├── TaskStats.scala │ │ │ ├── ResourceUsage.scala │ │ │ ├── MemoryStats.scala │ │ │ ├── Task.scala │ │ │ ├── CpuStats.scala │ │ │ ├── LogStreamKind.scala │ │ │ ├── TaskState.scala │ │ │ ├── AllocationStats.scala │ │ │ ├── ClientStatus.scala │ │ │ ├── Node.scala │ │ │ ├── Resources.scala │ │ │ └── Allocation.scala │ │ ├── package.scala │ │ ├── UnexpectedNomadHttpApiError.scala │ │ ├── NomadConfiguration.scala │ │ └── NomadModule.scala │ │ ├── signal │ │ ├── SignalModule.scala │ │ ├── SignalManager.scala │ │ └── UnixSignalManager.scala │ │ ├── websocket │ │ └── WebSocketConfiguration.scala │ │ ├── auth │ │ ├── BroccoliSecurity.scala │ │ ├── DefaultEnv.scala │ │ ├── AuthMode.scala │ │ ├── BroccoliFingerprintGenerator.scala │ │ ├── Account.scala │ │ ├── InMemoryIdentityService.scala │ │ └── AuthConfiguration.scala │ │ ├── http │ │ ├── Filters.scala │ │ └── AccessControlFilter.scala │ │ ├── templates │ │ ├── TemplateConfiguration.scala │ │ ├── SignalRefreshedTemplateSource.scala │ │ ├── CachedTemplateSource.scala │ │ ├── jinjava │ │ │ ├── JinjavaConfiguration.scala │ │ │ └── JinjavaModule.scala │ │ └── TemplateModule.scala │ │ ├── BroccoliConfiguration.scala │ │ ├── routes │ │ ├── Extractors.scala │ │ └── DownloadsRouter.scala │ │ ├── BroccoliModule.scala │ │ ├── RemoveSecrets.scala │ │ └── logging.scala │ ├── test │ ├── resources │ │ └── de │ │ │ └── frosner │ │ │ └── broccoli │ │ │ └── templates │ │ │ ├── curl-without-decription │ │ │ └── template.conf │ │ │ └── curl │ │ │ └── template.conf │ └── scala │ │ └── de │ │ └── frosner │ │ └── broccoli │ │ ├── services │ │ └── WebSocketServiceSpec.scala │ │ ├── models │ │ ├── InstanceStatusSpec.scala │ │ ├── ServiceStatusSpec.scala │ │ ├── ServiceSpec.scala │ │ └── InstanceTasksSpec.scala │ │ ├── util │ │ ├── Resources.scala │ │ └── TemporaryDirectoryContext.scala │ │ ├── templates │ │ ├── TemporaryTemplatesContext.scala │ │ ├── CachedTemplateSourceSpec.scala │ │ └── SignalRefreshedTemplateSourceSpec.scala │ │ ├── nomad │ │ └── models │ │ │ ├── NodeSpec.scala │ │ │ └── AllocationSpec.scala │ │ ├── signal │ │ └── UnixSignalManagerSpec.scala │ │ ├── http │ │ └── ToHTTPResultSpec.scala │ │ └── LoggingSpec.scala │ └── it │ └── scala │ └── de │ └── frosner │ └── broccoli │ ├── signal │ └── UnixSignalManagerIntegrationSpec.scala │ └── test │ └── contexts │ ├── WSClientContext.scala │ └── docker │ └── BroccoliTestService.scala ├── docker └── test │ ├── cluster-broccoli-files │ ├── broccoli.global.nomad.jks │ ├── couchdb-tls.conf │ ├── application-tls.conf │ └── nomad-ca.pem │ ├── nomad-server-config │ ├── server.hcl │ └── ssl │ │ └── server.pem │ ├── couchdb.conf │ └── Dockerfiletls ├── templates ├── jupyter │ └── template.conf ├── http-server │ └── template.conf ├── http-server-hcl │ └── template.conf └── curl │ └── template.conf ├── .scalafmt.conf ├── .gitignore ├── prepare-docker-builds └── script └── instances-0.6.0-to-0.7.0.sh /webui/tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.18 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-about/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/root-reachable/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-delete/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-start/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-stop/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-list/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/root-reachable/url: -------------------------------------------------------------------------------- 1 | localhost:9000 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-delete/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-start/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-stop/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-about/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-list-empty/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-404/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show-404/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/root-reachable/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-delete/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-start/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-stop/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-about/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/about 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-create/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-delete/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-edit/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-instances-service-status/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-delete/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-start/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-start/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-stop/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-404/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-404/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-list/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show-404/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-create-many/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-delete/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-start/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-service-status-unknown/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-start/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-stop/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-no-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-service-status-unknown/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-list-empty/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-list-empty/expected/response-data: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-create-many/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-edit/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-start/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-delete/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-start/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-stop/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/expected/http-status: -------------------------------------------------------------------------------- 1 | 400 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-create/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-delete/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-edit/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-list/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/templates 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-create/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-delete/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-edit/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-create/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-delete/method: -------------------------------------------------------------------------------- 1 | GET 2 | -------------------------------------------------------------------------------- /scalafmt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Data-Science-Platform/cluster-broccoli/HEAD/scalafmt -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-instances-service-status/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/curl-options: -------------------------------------------------------------------------------- 1 | -k 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-start/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-service-status-unknown/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-404/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/method: -------------------------------------------------------------------------------- 1 | POST 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-list-empty/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/templates/jupyter 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-create-many/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-start/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-service-status-unknown/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-stop/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-no-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-restart/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker stop $(cat cluster-broccoli.did) 3 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/expected/http-status: -------------------------------------------------------------------------------- 1 | 400 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/expected/http-status: -------------------------------------------------------------------------------- 1 | 400 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-create-many/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-create/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-delete/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-edit/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-delete/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-start/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-start/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-no-restart/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/expected/http-status: -------------------------------------------------------------------------------- 1 | 400 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-404/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/nonexisting 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show-404/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/templates/nonexisting 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-create/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-delete/expected/http-status: -------------------------------------------------------------------------------- 1 | 404 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-edit/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-create-many/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-instances-service-status/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-restart/url: -------------------------------------------------------------------------------- 1 | localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/expected/http-status: -------------------------------------------------------------------------------- 1 | 200 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-create/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-delete/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-edit/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-create-many/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-404/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-404/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test 2 | -------------------------------------------------------------------------------- /server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ## Your configuration goes here. See reference.conf for more details 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-start/url: -------------------------------------------------------------------------------- 1 | https://localhost:4646/v1/job/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-service-status-unknown/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-service-status-unknown/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-edit/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /webui/src/Commands/Fetch.elm: -------------------------------------------------------------------------------- 1 | module Commands.Fetch exposing (apiBaseUrl) 2 | 3 | 4 | apiBaseUrl = 5 | "/api/v1" 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-create/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-delete/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-edit/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-create/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-delete/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /webui/src/favicon-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Data-Science-Platform/cluster-broccoli/HEAD/webui/src/favicon-300.png -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/request-header: -------------------------------------------------------------------------------- 1 | Content-Type: application/json 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/url: -------------------------------------------------------------------------------- 1 | localhost:9000/api/v1/instances/test-http 2 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker stop $(cat cluster-broccoli.did) 4 | docker stop $(cat nomad.did) 5 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '"CPU":50' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep -s 'jupyter' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-about/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '{"project":{"name":"Cluster Broccoli"' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker stop $(cat cluster-broccoli.did) 4 | docker stop $(cat nomad.did) 5 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '"CPU":50' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep -s 'jupyter' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-edit/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '"cpu":50' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker stop $(cat cluster-broccoli.did) 3 | sudo rm -rf /tmp/instances 4 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-edit/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '"cpu":50' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep -s 'SimpleHTTPServer' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-no-restart/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep -s 'SimpleHTTPServer' $1 4 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker stop $(cat cluster-broccoli.did) 3 | docker stop $(cat couchdb.did) 4 | -------------------------------------------------------------------------------- /webui/src/Utils/ListUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.ListUtils exposing (remove) 2 | 3 | 4 | remove i xs = 5 | List.take i xs ++ List.drop (i + 1) xs 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "parameterValues": { 3 | "id": "new-id", 4 | "cpu": "50" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "parameterValues": { 3 | "id": "test-http" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /webui/.gitignore: -------------------------------------------------------------------------------- 1 | # Elm build artifacts and packages 2 | /elm-stuff/ 3 | 4 | # Webpack build artifacts 5 | /dist/ 6 | 7 | *.log 8 | 9 | /node_modules/ 10 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "parameterValues": { 3 | "id": "test-http", 4 | "cpu": 50 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-404/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "parameterValues": { 3 | "id": "test-http", 4 | "cpu": 50 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/after-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker stop $(cat cluster-broccoli.did) 4 | docker stop $(cat nomad.did) 5 | docker stop $(cat consul.did) 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "selectedTemplate": "jupyter", 3 | "parameterValues": { 4 | "id": "test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "parameterValues": { 3 | "id": "test-http", 4 | "notExisting": "50" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "selectedTemplate": "notExisting", 3 | "parameterValues": { 4 | "id": "test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docker/test/cluster-broccoli-files/broccoli.global.nomad.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Data-Science-Platform/cluster-broccoli/HEAD/docker/test/cluster-broccoli-files/broccoli.global.nomad.jks -------------------------------------------------------------------------------- /webui/src/Models/Ui/Notifications.elm: -------------------------------------------------------------------------------- 1 | module Models.Ui.Notifications exposing (..) 2 | 3 | 4 | type alias Error = 5 | String 6 | 7 | 8 | type alias Errors = 9 | List Error 10 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/request-data: -------------------------------------------------------------------------------- 1 | { 2 | "selectedTemplate": "http-server", 3 | "parameterValues": { 4 | "id": "schmeid" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/TemplateNotFoundException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | case class TemplateNotFoundException(id: String) extends Exception(s"Template '$id' not found") 4 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/InstanceNotFoundException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances 2 | 3 | case class InstanceNotFoundException(id: String) extends Exception(s"Instance '$id' not found") 4 | -------------------------------------------------------------------------------- /templates/jupyter/template.conf: -------------------------------------------------------------------------------- 1 | description = "Open source, interactive data science and scientific computing across over 40 programming languages." 2 | parameters { 3 | id { 4 | type { 5 | name = "string" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/conf/IllegalConfigException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.conf 2 | 3 | case class IllegalConfigException(property: String, reason: String) 4 | extends Exception(s"Illegal value of $property: $reason") 5 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | docker run --rm -d --net host frosner/cluster-broccoli-test cluster-broccoli > cluster-broccoli.did 4 | sleep $BROCCOLI_SLEEP_MEDIUM 5 | check_service http localhost 9000 6 | -------------------------------------------------------------------------------- /webui/src/Utils/DecodeUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.DecodeUtils exposing (maybeNull) 2 | 3 | import Json.Decode as Decode 4 | 5 | 6 | maybeNull decoder = 7 | Decode.oneOf 8 | [ Decode.null Nothing 9 | , Decode.map Just decoder 10 | ] 11 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/PrefixViolationException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | case class PrefixViolationException(id: String, prefix: String) 4 | extends Throwable(s"ID '$id' did not have the required prefix '$prefix'") 5 | -------------------------------------------------------------------------------- /webui/src/Models/Ui/LoginForm.elm: -------------------------------------------------------------------------------- 1 | module Models.Ui.LoginForm exposing (LoginForm, empty) 2 | 3 | 4 | type alias LoginForm = 5 | { username : String 6 | , password : String 7 | , loginIncorrect : Bool 8 | } 9 | 10 | 11 | empty = 12 | LoginForm "" "" False 13 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/NomadRequestFailed.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | case class NomadRequestFailed(url: String, status: Int, reason: String = "") 4 | extends Exception(s"Nomad request $url failed. Status code $status. Reason $reason") 5 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/PeriodicJobNotFoundException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances 2 | 3 | case class PeriodicJobNotFoundException(instanceId: String, jobId: String) 4 | extends Exception(s"Periodic job '$jobId' not found for instance '$instanceId'") 5 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameter-id/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-notExisting/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-create/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-invalidParameters/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /server/src/test/resources/de/frosner/broccoli/templates/curl-without-decription/template.conf: -------------------------------------------------------------------------------- 1 | parameters = { 2 | "id" = { 3 | type = raw 4 | } 5 | "URL" = { 6 | default = "localhost:8000" 7 | type = raw 8 | } 9 | "enabled" = { 10 | default = true 11 | type = raw 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | style = default 2 | # Format all tracked files 3 | project.git = true 4 | # Longer lines, yay 5 | maxColumn = 120 6 | # Sort import selectors, remove redundant parens and braces, and 7 | # use curly braces for comprehensions 8 | rewrite.rules = [SortImports, RedundantParens, RedundantBraces, PreferCurlyFors] 9 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-notExisting/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/controllers/StatusController.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.controllers 2 | 3 | import play.api.mvc.{Action, AnyContent, Controller, Results} 4 | 5 | class StatusController extends Controller { 6 | 7 | def status: Action[AnyContent] = Action(Results.Ok) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/Refresh.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import play.api.libs.json.Json 4 | 5 | case class RefreshRequest(token: String, returnTemplates: Boolean) 6 | 7 | object Refresh { 8 | implicit val refreshRequestReads = Json.reads[RefreshRequest] 9 | } 10 | -------------------------------------------------------------------------------- /docker/test/nomad-server-config/server.hcl: -------------------------------------------------------------------------------- 1 | tls { 2 | http = true 3 | rpc = true 4 | 5 | ca_file = "/nomad-ca.pem" 6 | cert_file = "/etc/nomad.d/ssl/server.pem" 7 | key_file = "/etc/nomad.d/ssl/server-key.pem" 8 | 9 | verify_server_hostname = false 10 | verify_https_client = false 11 | } 12 | 13 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-missing-with-default/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/InvalidWebsocketConnectionException.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | case class InvalidWebsocketConnectionException(id: String, connections: Iterable[String]) 4 | extends Exception(s"Connection $id is not in the pool of websocket connections: ${connections.mkString(", ")}") 5 | -------------------------------------------------------------------------------- /docker/test/couchdb.conf: -------------------------------------------------------------------------------- 1 | # Use couchdb for play 2 | play.crypto.secret = "yJEqZLcZQyWXfgYvhMbRqwakspWpT6oFnNLgTtmzVazrvVykQPaRWhNZDNwUZWbA" 3 | 4 | play.modules.disabled += "de.frosner.broccoli.instances.storage.filesystem.FileSystemStorageModule" 5 | play.modules.enabled += "de.frosner.broccoli.instances.storage.couchdb.CouchDBStorageModule" 6 | 7 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/WithId.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | /** 4 | * Attaches a nomad ID to arbitrary payload. 5 | * 6 | * @param jobId The nomad ID 7 | * @param payload The payload 8 | * @tparam T The type of the paylo 9 | */ 10 | final case class WithId[T](jobId: String, payload: T) 11 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | docker run --rm -d --net host frosner/cluster-broccoli-test nomad > nomad.did 4 | sleep $BROCCOLI_SLEEP_MEDIUM 5 | docker run --rm -d --net host frosner/cluster-broccoli-test cluster-broccoli > cluster-broccoli.did 6 | sleep $BROCCOLI_SLEEP_MEDIUM 7 | check_service http localhost 9000 8 | -------------------------------------------------------------------------------- /webui/src/Utils/TaskUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.TaskUtils exposing (delay) 2 | 3 | import Process 4 | import Task exposing (Task) 5 | import Time exposing (Time) 6 | 7 | 8 | {-| Delay a task a given amount of `Time` 9 | -} 10 | delay : Time -> Task error value -> Task error value 11 | delay time task = 12 | Process.sleep time 13 | |> Task.andThen (\_ -> task) 14 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-delete/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -X DELETE 'localhost:9000/api/v1/instances/test-http' 7 | sleep $BROCCOLI_SLEEP_SHORT 8 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | 4 | mkdir /tmp/instances 5 | docker run --rm -d --net host \ 6 | -v /tmp/instances:/cluster-broccoli-dist/instances \ 7 | frosner/cluster-broccoli-test \ 8 | cluster-broccoli > cluster-broccoli.did 9 | sleep $BROCCOLI_SLEEP_MEDIUM 10 | check_service http localhost 9000 11 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/signal/SignalModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.signal 2 | 3 | import com.google.inject.AbstractModule 4 | import net.codingwell.scalaguice.ScalaModule 5 | 6 | class SignalModule extends AbstractModule with ScalaModule { 7 | override def configure(): Unit = 8 | bind[SignalManager].to[UnixSignalManager].asEagerSingleton() 9 | } 10 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/services/WebSocketServiceSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | class WebSocketServiceSpec extends Specification { 6 | 7 | "Creating a new connection" should { 8 | 9 | "create" in { 10 | // TODO 11 | true === true 12 | } 13 | 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/websocket/WebSocketConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.websocket 2 | 3 | import scala.concurrent.duration.Duration 4 | 5 | /** 6 | * Configuration for Broccoli's websocket 7 | * 8 | * @param cacheTimeout The timeout for the websocket message cache 9 | */ 10 | final case class WebSocketConfiguration(cacheTimeout: Duration) 11 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/BroccoliSecurity.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import com.mohiva.play.silhouette.api.{Environment, Silhouette} 4 | 5 | trait BroccoliSecurity { 6 | type AuthenticityToken = String 7 | type SignedToken = String 8 | 9 | def silhouette: Silhouette[DefaultEnv] 10 | def env: Environment[DefaultEnv] = silhouette.env 11 | 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/InstanceConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances 2 | 3 | import de.frosner.broccoli.instances.storage.StorageConfiguration 4 | 5 | /** 6 | * Instance Configuration 7 | * 8 | * @param storage Configuration specific to the instance storage type 9 | */ 10 | final case class InstanceConfiguration(storage: StorageConfiguration) 11 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/storage/filesystem/FileSystemConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances.storage.filesystem 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Configuration for FileSystemInstanceStorage 7 | * 8 | * @param path location on the filesystem to store the instance information 9 | */ 10 | final case class FileSystemConfiguration(path: Path) 11 | -------------------------------------------------------------------------------- /docker/test/Dockerfiletls: -------------------------------------------------------------------------------- 1 | FROM frosner/cluster-broccoli-test:latest 2 | 3 | ## nomad certificates and configs 4 | COPY ./nomad-server-config/ /etc/nomad.d/ 5 | 6 | RUN echo "#!/bin/bash" > /usr/bin/nomad && \ 7 | echo "exec /nomad agent -dev -config=/etc/nomad.d/" >> /usr/bin/nomad && \ 8 | chmod 777 /usr/bin/nomad 9 | 10 | ## cluster-broccoli certificates and configs 11 | COPY ./cluster-broccoli-files/ / 12 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/storage/couchdb/CouchDBConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances.storage.couchdb 2 | 3 | /** 4 | * Configuration for couchdb instance storage. 5 | * 6 | * @param url database address 7 | * @param database database name where the instance information will be stored 8 | */ 9 | final case class CouchDBConfiguration(url: String, database: String) 10 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/LogKind.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.LogKind exposing (..) 2 | 3 | {-| Provides log kind 4 | -} 5 | 6 | 7 | {-| The kind of log to view 8 | -} 9 | type LogKind 10 | = StdOut 11 | | StdErr 12 | 13 | 14 | toParameter : LogKind -> String 15 | toParameter kind = 16 | case kind of 17 | StdOut -> 18 | "stdout" 19 | 20 | StdErr -> 21 | "stderr" 22 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/package.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import cats.data.EitherT 4 | import de.frosner.broccoli.nomad.models.NomadError 5 | 6 | import scala.concurrent.Future 7 | 8 | package object nomad { 9 | 10 | /** 11 | * Monad for Nomad actions. 12 | * 13 | * @tparam R The result type. 14 | */ 15 | type NomadT[R] = EitherT[Future, NomadError, R] 16 | 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | # Artifacts from building docker distributions 4 | /docker/test/cluster-broccoli-dist/ 5 | /docker/test/templates 6 | 7 | # Docker process IDs generated when running integration tests 8 | /cluster-broccoli.did 9 | /nomad.did 10 | /consul.did 11 | /couchdb.did 12 | 13 | # Instance storage for dev mode 14 | /instances/ 15 | 16 | # results from HTTP API testing tool 17 | *run-* 18 | 19 | .metals/* 20 | .vscode/* 21 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/TaskLog.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import shapeless.tag.@@ 4 | 5 | /** 6 | * The log of a task. 7 | * 8 | * @param kind The kind of log 9 | * @param contents The log 10 | */ 11 | final case class TaskLog(kind: LogStreamKind, contents: String @@ TaskLog.Contents) 12 | 13 | object TaskLog { 14 | sealed trait Offset 15 | sealed trait Contents 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceCreation.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import play.api.libs.json.{JsValue, Json} 4 | 5 | case class InstanceCreation(templateId: String, parameters: Map[String, JsValue]) 6 | 7 | object InstanceCreation { 8 | 9 | implicit val instanceCreationWrites = Json.writes[InstanceCreation] 10 | 11 | implicit val instanceCreationReads = Json.reads[InstanceCreation] 12 | 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/TaskStateEvents.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json.{JsPath, Reads} 5 | 6 | final case class TaskStateEvents(state: TaskState) 7 | 8 | object TaskStateEvents { 9 | 10 | implicit val taskStateEventsReads: Reads[TaskStateEvents] = 11 | (JsPath \ "State").read[TaskState].map(TaskStateEvents(_)) 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/http/Filters.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.http 2 | 3 | import javax.inject.Inject 4 | 5 | import play.api.http.DefaultHttpFilters 6 | 7 | /** 8 | * Provide all HTTP filters for Broccoli. 9 | * 10 | * @param accessControlFilter A filter to setup access control for Broccoli 11 | */ 12 | class Filters @Inject()(accessControlFilter: AccessControlFilter) extends DefaultHttpFilters(accessControlFilter) {} 13 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Service.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json._ 4 | import play.api.libs.functional.syntax._ 5 | 6 | final case class Service(name: String) 7 | 8 | object Service { 9 | 10 | implicit val serviceFormat: Format[Service] = 11 | (__ \ "Name") 12 | .format[String] 13 | .inmap(name => Service(name), (service: Service) => service.name) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/UnexpectedNomadHttpApiError.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad 2 | 3 | import play.api.libs.ws.WSResponse 4 | 5 | /** 6 | * An unexpected and unhandled response from the Nomad API. 7 | * 8 | * @param response The original response 9 | */ 10 | class UnexpectedNomadHttpApiError(val response: WSResponse) 11 | extends Exception(s"Unexpected Nomad response: ${response.status} ${response.statusText}") {} 12 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-templates-show/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "jupyter", 3 | "description": "Open source, interactive data science and scientific computing across over 40 programming languages.", 4 | "parameters": [ 5 | "id" 6 | ], 7 | "parameterInfos": { 8 | "id": { 9 | "id": "id", 10 | "type": { 11 | "name": "string" 12 | } 13 | } 14 | }, 15 | "version": "6540294bd9a0065990bf9daa5dd2824b" 16 | } -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-create-many/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | actual_num_entries="$(cat $1 | jq '. | length')" 4 | expected_num_entries=500 5 | if [ "$actual_num_entries" -eq "$expected_num_entries" ] 6 | then 7 | echo -e "\033[0;32mGot $expected_num_entries entries.\033[0m" 8 | exit 0 9 | else 10 | echo -e "\033[0;31mExpected $expected_num_entries entries but got $actual_num_entries.\033[0m" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Enable partial unification of types, for various scala versions 2 | addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.1.0") 3 | 4 | // Play framework 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.20") 6 | 7 | // Build metadata available at runtime 8 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.6.1") 9 | 10 | // Docker packaging for Broccoli 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.1") 12 | -------------------------------------------------------------------------------- /webui/src/Utils/CmdUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.CmdUtils exposing (delayMsg, sendMsg) 2 | 3 | import Task 4 | import Time exposing (Time) 5 | import Utils.TaskUtils as TaskUtils 6 | 7 | 8 | sendMsg : msg -> Cmd msg 9 | sendMsg message = 10 | Task.perform identity (Task.succeed message) 11 | 12 | 13 | delayMsg : Time -> msg -> Cmd msg 14 | delayMsg time message = 15 | Task.succeed message 16 | |> TaskUtils.delay time 17 | |> Task.perform identity 18 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-examples-http-server/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test' 9 | sleep $BROCCOLI_SLEEP_MEDIUM 10 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-create-many/expected/response-data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | actual_num_entries="$(cat $1 | jq '. | length')" 4 | expected_num_entries=500 5 | if [ "$actual_num_entries" -eq "$expected_num_entries" ] 6 | then 7 | echo -e "\033[0;32mGot $expected_num_entries entries.\033[0m" 8 | exit 0 9 | else 10 | echo -e "\033[0;31mExpected $expected_num_entries entries but got $actual_num_entries.\033[0m" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /server/src/test/resources/de/frosner/broccoli/templates/curl/template.conf: -------------------------------------------------------------------------------- 1 | description = "A periodic job that sends an HTTP GET request to a specified address every minute." 2 | 3 | parameters = { 4 | "id" = { 5 | type = raw 6 | order-index = 0 7 | } 8 | "URL" = { 9 | default = "localhost:8000", 10 | order-index = 1 11 | type = raw 12 | } 13 | "enabled" = { 14 | default = true, 15 | order-index = 2 16 | type = raw 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-start/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-edit/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -v -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-start/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/NomadConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad 2 | 3 | /** 4 | * The Nomad configuration. 5 | * 6 | * @param url The URL to connect to to access nomad via HTTP. 7 | */ 8 | final case class NomadConfiguration(url: String, 9 | tokenEnvName: String, 10 | namespacesEnabled: Boolean, 11 | namespaceVariable: String) 12 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-service-status-unknown/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_MEDIUM 10 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-service-status-unknown/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_MEDIUM 10 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | 4 | docker pull couchdb:1.7.0 5 | docker run --rm -d --net host couchdb:1.7.0 > couchdb.did 6 | 7 | sleep $BROCCOLI_SLEEP_MEDIUM 8 | 9 | docker run --rm -d --net host \ 10 | frosner/cluster-broccoli-test \ 11 | cluster-broccoli \ 12 | -Dconfig.file="/couchdb.conf" > cluster-broccoli.did 13 | 14 | sleep $BROCCOLI_SLEEP_MEDIUM 15 | 16 | check_service http localhost 9000 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/TaskGroup.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json._ 4 | import play.api.libs.functional.syntax._ 5 | 6 | final case class TaskGroup(tasks: Seq[Task]) 7 | 8 | object TaskGroup { 9 | 10 | implicit val taskGroupFormat: Format[TaskGroup] = 11 | (__ \ "Tasks") 12 | .format[Seq[Task]] 13 | .inmap(tasks => TaskGroup(tasks), (taskGroup: TaskGroup) => taskGroup.tasks) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/NomadError.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | /** 4 | * Errors from the Nomad API. 5 | */ 6 | sealed trait NomadError 7 | 8 | object NomadError { 9 | 10 | /** 11 | * A Nomad object (job, allocation, etc.) was not found 12 | */ 13 | final case object NotFound extends NomadError 14 | 15 | /** 16 | * Nomad was not reachable 17 | */ 18 | final case object Unreachable extends NomadError 19 | } 20 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Job.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json._ 4 | import play.api.libs.functional.syntax._ 5 | 6 | final case class Job(taskGroups: Seq[TaskGroup]) 7 | 8 | object Job { 9 | sealed trait Id 10 | 11 | implicit val jobFormat: Format[Job] = 12 | (JsPath \ "TaskGroups") 13 | .format[Seq[TaskGroup]] 14 | .inmap(taskGroups => Job(taskGroups), (job: Job) => job.taskGroups) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-instances-service-status/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test' 9 | sleep $BROCCOLI_SLEEP_LONG 10 | sleep $BROCCOLI_SLEEP_LONG 11 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/TaskStats.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json.{JsPath, Reads} 4 | 5 | /** 6 | * Resource usage of an individual task. 7 | * 8 | * @param resourceUsage The resource usage 9 | */ 10 | final case class TaskStats(resourceUsage: ResourceUsage) 11 | 12 | object TaskStats { 13 | implicit val taskStatsReads: Reads[TaskStats] = (JsPath \ "ResourceUsage").read[ResourceUsage].map(TaskStats(_)) 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/TemplateConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import de.frosner.broccoli.templates.jinjava.JinjavaConfiguration 4 | 5 | /** 6 | * The Templates configuration. 7 | * 8 | * @param path The filesystem path to read the templates from 9 | * @param jinjava Configuration specific to jinjava template library 10 | */ 11 | final case class TemplateConfiguration(path: String, jinjava: JinjavaConfiguration, reloadToken: String) 12 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/ResourceUsage.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json.{JsPath, Reads} 4 | import play.api.libs.functional.syntax._ 5 | 6 | final case class ResourceUsage(cpuStats: CpuStats, memoryStats: MemoryStats) 7 | 8 | object ResourceUsage { 9 | implicit val resourceUsageReads: Reads[ResourceUsage] = 10 | ((JsPath \ "CpuStats").read[CpuStats] and (JsPath \ "MemoryStats").read[MemoryStats])(ResourceUsage.apply _) 11 | } 12 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/api-v1-examples-http-server/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | service_address=$(curl -s localhost:9000/api/v1/instances/test | jq -r '."services" | map(select(."name" == "test-web-ui-1"))[0]."address"') 4 | service_port=$(curl -s localhost:9000/api/v1/instances/test | jq -r '."services" | map(select(."name" == "test-web-ui-1"))[0]."port"') 5 | service_socket=$service_address:$service_port 6 | 7 | echo " - Waiting for service $service_socket to come up ..." 8 | curl -s $service_socket > /dev/null 9 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/conf/package.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | package object conf { 4 | 5 | val CONSUL_URL_KEY = "broccoli.consul.url" 6 | val CONSUL_URL_DEFAULT = "http://localhost:8500" 7 | 8 | val CONSUL_LOOKUP_METHOD_KEY = "broccoli.consul.lookup" 9 | val CONSUL_DOMAIN_URL_KEY = "broccoli.consul.domain-url" 10 | val CONSUL_DOMAIN_PATH_KEY = "broccoli.consul.domain-path" 11 | val CONSUL_LOOKUP_METHOD_IP = "ip" 12 | val CONSUL_LOOKUP_METHOD_DNS = "dns" 13 | 14 | } 15 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/Allocation.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.Allocation exposing (..) 2 | 3 | 4 | type alias AllocationId = 5 | String 6 | 7 | 8 | {-| Get the short ID of an allocation. 9 | 10 | Nomad uses UUIDs as allocation IDs and allows to refer to allocations with a 11 | short ID, ie, the first component of the UUID ("up to the first dash"). 12 | 13 | -} 14 | shortAllocationId : AllocationId -> AllocationId 15 | shortAllocationId id = 16 | String.split "-" id |> List.head |> Maybe.withDefault id 17 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | docker run --rm -d --net host --privileged=true --hostname nomad-server.integrationtest frosner/cluster-broccoli-test-tls nomad > nomad.did 4 | sleep $BROCCOLI_SLEEP_MEDIUM 5 | docker run --rm -d --net host frosner/cluster-broccoli-test-tls cluster-broccoli -Dconfig.file="/application-tls.conf" -Dbroccoli.nomad.url=https://localhost:4646 > cluster-broccoli.did 6 | sleep $BROCCOLI_SLEEP_MEDIUM 7 | check_service http localhost 9000 8 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/PeriodicRun.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.models.JobStatus.JobStatus 4 | import play.api.libs.json.Json 5 | import JobStatusJson._ 6 | 7 | case class PeriodicRun(createdBy: String, status: JobStatus, utcSeconds: Long, jobName: String) extends Serializable 8 | 9 | object PeriodicRun { 10 | 11 | implicit val periodicRunWrites = Json.writes[PeriodicRun] 12 | 13 | implicit val periodicRunReads = Json.reads[PeriodicRun] 14 | 15 | } -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/Service.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.models.ServiceStatus.ServiceStatus 4 | import ServiceStatusJson._ 5 | import play.api.libs.json.Json 6 | 7 | case class Service(name: String, protocol: String, address: String, port: Int, status: ServiceStatus) 8 | extends Serializable 9 | 10 | object Service { 11 | 12 | implicit val serviceWrites = Json.writes[Service] 13 | 14 | implicit val serviceReads = Json.reads[Service] 15 | 16 | } 17 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/models/InstanceStatusSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import org.specs2.mutable.Specification 4 | import play.api.libs.json.Json 5 | import JobStatusJson.{jobStatusReads, jobStatusWrites} 6 | 7 | class InstanceStatusSpec extends Specification { 8 | 9 | "Instance status JSON serialization" should { 10 | 11 | "work" in { 12 | val status = JobStatus.Running 13 | Json.fromJson(Json.toJson(status)).get === status 14 | } 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/resources/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # API routes 6 | -> /api/v1 de.frosner.broccoli.routes.ApiV1Router 7 | 8 | # Downloads 9 | -> /downloads/ de.frosner.broccoli.routes.DownloadsRouter 10 | 11 | # Web-UI 12 | GET / @controllers.Assets.at(path="/public", file="index.html") 13 | GET /ws @de.frosner.broccoli.controllers.WebSocketController.socket 14 | GET /*file @controllers.Assets.versioned(path="/public", file) 15 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/models/ServiceStatusSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import org.specs2.mutable.Specification 4 | import play.api.libs.json.Json 5 | import ServiceStatusJson.{serviceStatusReads, serviceStatusWrites} 6 | 7 | class ServiceStatusSpec extends Specification { 8 | 9 | "Service status JSON serialization" should { 10 | 11 | "work" in { 12 | val status = ServiceStatus.Passing 13 | Json.fromJson(Json.toJson(status)).get === status 14 | } 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-consul/before-each: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../common.sh 3 | docker run --rm -d --net host frosner/cluster-broccoli-test consul > consul.did 4 | sleep $BROCCOLI_SLEEP_MEDIUM 5 | docker run --rm -d --net host -v /var/run/docker.sock:/var/run/docker.sock frosner/cluster-broccoli-test nomad > nomad.did 6 | sleep $BROCCOLI_SLEEP_MEDIUM 7 | docker run --rm -d --net host frosner/cluster-broccoli-test cluster-broccoli > cluster-broccoli.did 8 | sleep $BROCCOLI_SLEEP_MEDIUM 9 | check_service http localhost 9000 10 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/DefaultEnv.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import com.mohiva.play.silhouette.api.Env 4 | import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator 5 | 6 | class DefaultEnv extends Env { 7 | 8 | /** Identity 9 | */ 10 | type I = Account 11 | 12 | /** Authenticator used for identification. 13 | * [[com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator]] could've also been used for REST. 14 | */ 15 | type A = CookieAuthenticator 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/AuthMode.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import enumeratum.EnumEntry.Lowercase 4 | import enumeratum._ 5 | 6 | import scala.collection.immutable 7 | 8 | /** 9 | * Authentication mode for Broccoli 10 | */ 11 | sealed trait AuthMode extends EnumEntry with Lowercase 12 | 13 | object AuthMode extends Enum[AuthMode] { 14 | override val values: immutable.IndexedSeq[AuthMode] = findValues 15 | 16 | final case object None extends AuthMode 17 | 18 | final case object Conf extends AuthMode 19 | } 20 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-delete/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -X DELETE 'localhost:9000/api/v1/instances/test-http' 11 | sleep $BROCCOLI_SLEEP_SHORT 12 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%level] [%thread] [%logger{36}] - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/TemplateFormat.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import enumeratum.{Enum, EnumEntry, PlayJsonEnum} 4 | 5 | import scala.collection.immutable 6 | 7 | sealed trait TemplateFormat extends EnumEntry with EnumEntry.Lowercase 8 | 9 | object TemplateFormat extends Enum[TemplateFormat] with PlayJsonEnum[TemplateFormat] { 10 | 11 | override val values: immutable.IndexedSeq[TemplateFormat] = findValues 12 | 13 | case object JSON extends TemplateFormat 14 | case object HCL extends TemplateFormat 15 | } 16 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceDeleted.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceDeleted exposing (InstanceDeleted, decoder) 2 | 3 | import Json.Decode as Decode exposing (field) 4 | import Models.Resources.Instance as Instance exposing (Instance, InstanceId) 5 | 6 | 7 | type alias InstanceDeleted = 8 | { instanceId : InstanceId 9 | , instance : Instance 10 | } 11 | 12 | 13 | decoder : Decode.Decoder InstanceDeleted 14 | decoder = 15 | Decode.map2 InstanceDeleted 16 | (field "instanceId" Decode.string) 17 | (field "instance" Instance.decoder) 18 | -------------------------------------------------------------------------------- /webui/src/Routing.elm: -------------------------------------------------------------------------------- 1 | module Routing exposing (..) 2 | 3 | import Model exposing (Route(..)) 4 | import Navigation exposing (Location) 5 | import UrlParser exposing (..) 6 | 7 | 8 | matchers : Parser (Route -> a) a 9 | matchers = 10 | oneOf 11 | [ map MainRoute top 12 | ] 13 | 14 | 15 | parseLocation : Location -> Route 16 | parseLocation location = 17 | case parseHash matchers location of 18 | Just route -> 19 | route 20 | 21 | Nothing -> 22 | MainRoute 23 | 24 | 25 | 26 | -- main route is always fine :) 27 | -------------------------------------------------------------------------------- /templates/http-server/template.conf: -------------------------------------------------------------------------------- 1 | description = "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests." 2 | parameters { 3 | cpu { 4 | name = "CPU Shares" 5 | default = 100 6 | type { 7 | name = "integer" 8 | } 9 | } 10 | secret { 11 | default = 123.456 12 | name = "A Secret Parameter" 13 | type { 14 | name = "decimal" 15 | } 16 | secret = true 17 | } 18 | id { 19 | type { 20 | name = "string" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceError.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceError exposing (InstanceError, decoder) 2 | 3 | import Json.Decode as Decode exposing (Decoder, field) 4 | import Models.Resources.Instance exposing (InstanceId) 5 | 6 | 7 | {-| An error occurred while performing an operation on an instance. 8 | -} 9 | type alias InstanceError = 10 | { reason : String 11 | } 12 | 13 | 14 | {-| Decode an instance error from JSON. 15 | -} 16 | decoder : Decoder InstanceError 17 | decoder = 18 | Decode.map InstanceError 19 | (field "reason" Decode.string) 20 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/UserInfo.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.UserInfo exposing (UserInfo, userInfoDecoder) 2 | 3 | import Json.Decode as Decode exposing (field) 4 | import Models.Resources.Role as Role exposing (Role) 5 | 6 | 7 | type alias UserInfo = 8 | { name : String 9 | , role : Role 10 | , instanceRegex : String 11 | } 12 | 13 | 14 | userInfoDecoder : Decode.Decoder UserInfo 15 | userInfoDecoder = 16 | Decode.map3 UserInfo 17 | (field "name" Decode.string) 18 | (field "role" Role.decoder) 19 | (field "instanceRegex" Decode.string) 20 | -------------------------------------------------------------------------------- /templates/http-server-hcl/template.conf: -------------------------------------------------------------------------------- 1 | description = "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests." 2 | parameters { 3 | cpu { 4 | name = "CPU Shares" 5 | default = 100 6 | type { 7 | name = "integer" 8 | } 9 | } 10 | secret { 11 | default = 123.456 12 | name = "A Secret Parameter" 13 | type { 14 | name = "decimal" 15 | } 16 | secret = true 17 | } 18 | id { 19 | type { 20 | name = "string" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/TemplateService.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import de.frosner.broccoli.models.Template 5 | import de.frosner.broccoli.templates._ 6 | 7 | @Singleton 8 | class TemplateService @Inject()(templateSource: TemplateSource) { 9 | def getTemplates: Seq[Template] = getTemplates(false) 10 | 11 | def getTemplates(refreshed: Boolean): Seq[Template] = templateSource.loadTemplates(refreshed) 12 | 13 | def template(id: String): Option[Template] = getTemplates.find(_.id == id) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/ParameterValueExceptions.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | case class ParameterValueParsingException(parameter: String, reason: String) 4 | extends Exception(s"Could not parse parameter $parameter. $reason") 5 | 6 | case class ParameterNotFoundException(parameter: String, availParams: Set[String]) 7 | extends Exception(s"Parameter '$parameter' not found. Available parameters are ${availParams.toString}") 8 | 9 | case class ParameterTypeException(message: String) extends Exception(s"Error parsing parameter metadata. $message") 10 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/util/Resources.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.util 2 | 3 | import scala.io.Source 4 | 5 | /** 6 | * Utilities for reading resources. 7 | */ 8 | object Resources { 9 | 10 | /** 11 | * Read a resource as string. 12 | * 13 | * @param path The resource path 14 | * @return The resource string 15 | */ 16 | def readAsString(path: String): String = { 17 | val stream = getClass.getResourceAsStream(path) 18 | try { 19 | Source.fromInputStream(stream, "UTF-8").mkString 20 | } finally { 21 | stream.close() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webui/src/Updates/UpdateErrors.elm: -------------------------------------------------------------------------------- 1 | module Updates.UpdateErrors exposing (updateErrors) 2 | 3 | import Messages exposing (AnyMsg) 4 | import Models.Ui.Notifications exposing (Errors) 5 | import Updates.Messages exposing (UpdateErrorsMsg(..)) 6 | import Utils.ListUtils as ListUtils 7 | 8 | 9 | updateErrors : UpdateErrorsMsg -> Errors -> ( Errors, Cmd AnyMsg ) 10 | updateErrors message oldErrors = 11 | case message of 12 | AddError error -> 13 | ( error :: oldErrors, Cmd.none ) 14 | 15 | CloseError index -> 16 | ( ListUtils.remove index oldErrors 17 | , Cmd.none 18 | ) 19 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/storage/StorageConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances.storage 2 | 3 | import de.frosner.broccoli.instances.storage.couchdb.CouchDBConfiguration 4 | import de.frosner.broccoli.instances.storage.filesystem.FileSystemConfiguration 5 | 6 | /** 7 | * Configuration specific to the instance storage type 8 | * 9 | * @param fs Configuration for FileSystemInstanceStorage 10 | * @param couchdb Configuration for CouchDBInstanceStorage 11 | */ 12 | final case class StorageConfiguration( 13 | fs: FileSystemConfiguration, 14 | couchdb: CouchDBConfiguration 15 | ) 16 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/models/ServiceSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import org.specs2.mutable.Specification 4 | import play.api.libs.json.Json 5 | import Service.{serviceReads, serviceWrites} 6 | 7 | class ServiceSpec extends Specification { 8 | 9 | "Service JSON serialization" should { 10 | 11 | "work" in { 12 | val service = Service( 13 | name = "s", 14 | protocol = "p", 15 | address = "a", 16 | port = 0, 17 | status = ServiceStatus.Unknown 18 | ) 19 | Json.fromJson(Json.toJson(service)).get === service 20 | } 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/PeriodicRun.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.PeriodicRun exposing (PeriodicRun, decoder) 2 | 3 | import Json.Decode as Decode exposing (field) 4 | import Models.Resources.JobStatus as JobStatus exposing (JobStatus) 5 | 6 | 7 | 8 | -- createdBy: String, status: JobStatus, utcSeconds: Long, jobName: String 9 | 10 | 11 | type alias PeriodicRun = 12 | { status : JobStatus 13 | , utcSeconds : Int 14 | , jobName : String 15 | } 16 | 17 | 18 | decoder = 19 | Decode.map3 PeriodicRun 20 | (field "status" JobStatus.decoder) 21 | (field "utcSeconds" Decode.int) 22 | (field "jobName" Decode.string) 23 | -------------------------------------------------------------------------------- /webui/src/Utils/DictUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.DictUtils exposing (flatMap, flatten) 2 | 3 | import Dict exposing (Dict) 4 | 5 | 6 | flatMap : (comparable -> b -> Maybe c) -> Dict comparable b -> Dict comparable c 7 | flatMap apply inDict = 8 | -- Using foldl as a flatMap since Elm does not have flatMap 9 | Dict.foldl 10 | (\k v acc -> 11 | Dict.update k (always (apply k v)) acc 12 | ) 13 | Dict.empty 14 | inDict 15 | 16 | 17 | flatten : Dict comparable (Maybe b) -> Dict comparable b 18 | flatten inDict = 19 | let 20 | mapFunc k v = 21 | v 22 | in 23 | flatMap mapFunc inDict 24 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-stop/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "status": "stopped" }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | -------------------------------------------------------------------------------- /templates/curl/template.conf: -------------------------------------------------------------------------------- 1 | description = "A periodic job that sends an HTTP GET request to a specified address every minute." 2 | parameters { 3 | URL { 4 | default = "localhost:8000" 5 | type { 6 | name = "raw" 7 | } 8 | order-index = 1 9 | } 10 | enabled { 11 | default = true 12 | type { 13 | name = "raw" 14 | } 15 | order-index = 2 16 | } 17 | id { 18 | type { 19 | name = "string" 20 | } 21 | } 22 | retries { 23 | type { 24 | name = "list" 25 | metadata = { 26 | provider = "StaticIntListProvider" 27 | values = [1, 2, 3, 4, 5] 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-create/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | curl -H 'Content-Type: application/json' \ 4 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 5 | 'http://localhost:9000/api/v1/instances' 6 | sleep $BROCCOLI_SLEEP_SHORT 7 | docker stop $(cat cluster-broccoli.did) 8 | sleep $BROCCOLI_SLEEP_SHORT 9 | docker run --rm -d --net host \ 10 | frosner/cluster-broccoli-test \ 11 | cluster-broccoli \ 12 | -Dconfig.file=/couchdb.conf > cluster-broccoli.did 13 | sleep $BROCCOLI_SLEEP_MEDIUM 14 | check_service http localhost 9000 15 | -------------------------------------------------------------------------------- /http-api-tests/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | check_service() { 4 | scheme=${1:-http} 5 | url=${2:-localhost} 6 | port=${3:-9000} 7 | echo "checking service at $scheme://$url:$port" 8 | attempt_counter=0 9 | max_attempts=${BROCCOLI_TIMEOUT_ATTEMPTS:-10} 10 | until $(curl --output /dev/null --silent --head --fail $scheme://$url:$port); do 11 | if [[ ${attempt_counter} -eq ${max_attempts} ]];then 12 | echo "Max attempts reached. Could not connect." 13 | exit 1 14 | fi 15 | attempt_counter=$(($attempt_counter+1)) 16 | printf '.' 17 | sleep $BROCCOLI_SLEEP_SHORT 18 | done 19 | echo "Broccoli started successfully." 20 | } -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/MemoryStats.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json.{JsPath, Reads} 4 | import shapeless.tag 5 | import shapeless.tag.@@ 6 | import squants.information.{Bytes, Information} 7 | 8 | /** 9 | * Memory statistics. 10 | * 11 | * @param rss Residual memory in bytes 12 | */ 13 | final case class MemoryStats(rss: Information @@ MemoryStats.RSS) 14 | 15 | object MemoryStats { 16 | sealed trait RSS 17 | 18 | implicit val memoryStatsReads: Reads[MemoryStats] = (JsPath \ "RSS") 19 | .read[Long] 20 | .map(bytes => tag[RSS](Bytes(bytes))) 21 | .map(MemoryStats(_)) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/ServiceStatus.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.ServiceStatus exposing (ServiceStatus(..), serviceStatusDecoder) 2 | 3 | import Json.Decode as Decode 4 | 5 | 6 | type ServiceStatus 7 | = ServicePassing 8 | | ServiceFailing 9 | | ServiceUnknown 10 | 11 | 12 | serviceStatusDecoder = 13 | Decode.andThen 14 | (\statusString -> Decode.succeed (stringToServiceStatus statusString)) 15 | Decode.string 16 | 17 | 18 | stringToServiceStatus s = 19 | case s of 20 | "passing" -> 21 | ServicePassing 22 | 23 | "failing" -> 24 | ServiceFailing 25 | 26 | _ -> 27 | ServiceUnknown 28 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-create/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | curl -H 'Content-Type: application/json' \ 4 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 5 | 'http://localhost:9000/api/v1/instances' 6 | sleep $BROCCOLI_SLEEP_SHORT 7 | docker stop $(cat cluster-broccoli.did) 8 | sleep $BROCCOLI_SLEEP_SHORT 9 | docker run --rm -d --net host \ 10 | -v /tmp/instances:/cluster-broccoli-dist/instances \ 11 | frosner/cluster-broccoli-test \ 12 | cluster-broccoli > cluster-broccoli.did 13 | sleep $BROCCOLI_SLEEP_MEDIUM 14 | check_service http localhost 9000 15 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-template-200/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test", 3 | "parameterValues": { 4 | "id": "test" 5 | }, 6 | "status": "unknown", 7 | "services": [], 8 | "periodicRuns": [], 9 | "template": { 10 | "id": "jupyter", 11 | "description": "Open source, interactive data science and scientific computing across over 40 programming languages.", 12 | "parameters": [ 13 | "id" 14 | ], 15 | "parameterInfos": { 16 | "id": { 17 | "id": "id", 18 | "type": { 19 | "name": "string" 20 | } 21 | } 22 | }, 23 | "version": "6540294bd9a0065990bf9daa5dd2824b" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/BroccoliConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import de.frosner.broccoli.auth.AuthConfiguration 4 | import de.frosner.broccoli.instances.InstanceConfiguration 5 | import de.frosner.broccoli.nomad.NomadConfiguration 6 | import de.frosner.broccoli.templates.TemplateConfiguration 7 | import de.frosner.broccoli.websocket.WebSocketConfiguration 8 | 9 | /** 10 | * The broccoli configuration. 11 | */ 12 | final case class BroccoliConfiguration( 13 | nomad: NomadConfiguration, 14 | templates: TemplateConfiguration, 15 | instances: InstanceConfiguration, 16 | auth: AuthConfiguration, 17 | webSocket: WebSocketConfiguration 18 | ) 19 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/Service.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.Service exposing (Service, decoder) 2 | 3 | import Json.Decode as Decode exposing (field) 4 | import Models.Resources.ServiceStatus as ServiceStatus exposing (ServiceStatus) 5 | 6 | 7 | type alias Service = 8 | { name : String 9 | , protocol : String 10 | , address : String 11 | , port_ : Int 12 | , status : ServiceStatus 13 | } 14 | 15 | 16 | decoder = 17 | Decode.map5 Service 18 | (field "name" Decode.string) 19 | (field "protocol" Decode.string) 20 | (field "address" Decode.string) 21 | (field "port" Decode.int) 22 | (field "status" ServiceStatus.serviceStatusDecoder) 23 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-start/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -v -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "status": "running" }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/ServiceStatus.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.models.ServiceStatus.ServiceStatus 4 | import play.api.libs.json._ 5 | 6 | object ServiceStatus extends Enumeration { 7 | 8 | type ServiceStatus = Value 9 | 10 | val Passing = Value("passing") 11 | val Failing = Value("failing") 12 | val Unknown = Value("unknown") 13 | 14 | } 15 | 16 | object ServiceStatusJson { 17 | 18 | implicit val serviceStatusWrites: Writes[ServiceStatus] = Writes(value => JsString(value.toString)) 19 | 20 | implicit val serviceStatusReads: Reads[ServiceStatus] = Reads(_.validate[String].map(ServiceStatus.withName)) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Task.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json._ 4 | import play.api.libs.functional.syntax._ 5 | import shapeless.tag 6 | import shapeless.tag.@@ 7 | 8 | final case class Task(name: String @@ Task.Name, resources: Resources, services: Option[Seq[Service]]) 9 | 10 | object Task { 11 | sealed trait Name 12 | 13 | implicit val taskFormat: Format[Task] = ( 14 | (JsPath \ "Name").format[String].inmap[String @@ Name](tag[Name](_), identity) 15 | and (JsPath \ "Resources").format[Resources] and 16 | (JsPath \ "Services").formatNullable[Seq[Service]] 17 | )(Task.apply, unlift(Task.unapply)) 18 | } 19 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-start/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -v -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "status": "running" }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-no-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | 7 | curl -H 'Content-Type: application/json' \ 8 | -X POST -d '{ "status": "running" }' \ 9 | 'http://localhost:9000/api/v1/instances/test' 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | 12 | curl -H 'Content-Type: application/json' \ 13 | -X POST -d '{ "selectedTemplate": "jupyter", "parameterValues": { "id": "test" } }' \ 14 | 'http://localhost:9000/api/v1/instances/test' 15 | sleep $BROCCOLI_SLEEP_SHORT 16 | -------------------------------------------------------------------------------- /webui/README.md: -------------------------------------------------------------------------------- 1 | ## Elm Front-End Code 2 | 3 | To build and package the front-end of Cluster Broccoli, you need to have Elm 4 | and NPM installed. 5 | 6 | ### Setup the Development Environment 7 | 8 | - `yarn install` 9 | - `yarn setup` 10 | 11 | ## Develop 12 | 13 | Start the backend with `sbt server/run` from the project root directory, on . 14 | 15 | Then run `yarn start` in this directory to run a hot-reloading development server for the frontend 16 | on . This server watches all files and automatically reloads when you make 17 | changes to the code. 18 | 19 | ### Run the Tests 20 | 21 | - `yarn test` 22 | 23 | ### Compile and Package 24 | 25 | - `yarn dist` 26 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-no-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | 7 | curl -H 'Content-Type: application/json' \ 8 | -X POST -d '{ "status": "running" }' \ 9 | 'http://localhost:9000/api/v1/instances/test' 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | 12 | curl -H 'Content-Type: application/json' \ 13 | -X POST -d '{ "selectedTemplate": "jupyter", "parameterValues": { "id": "test" } }' \ 14 | 'http://localhost:9000/api/v1/instances/test' 15 | sleep $BROCCOLI_SLEEP_SHORT 16 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/CpuStats.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json.{JsPath, Reads} 4 | import shapeless.tag 5 | import shapeless.tag.@@ 6 | import squants.time.{Frequency, Megahertz} 7 | 8 | /** 9 | * Statistics about CPU usage 10 | * 11 | * @param totalTicks The CPU ticks consumed 12 | */ 13 | final case class CpuStats(totalTicks: Frequency @@ CpuStats.TotalTicks) 14 | 15 | object CpuStats { 16 | sealed trait TotalTicks 17 | 18 | implicit val cpuStatsReads: Reads[CpuStats] = for { 19 | ticks <- (JsPath \ "TotalTicks").read[Double].map(ticks => tag[CpuStats.TotalTicks](Megahertz(ticks))) 20 | } yield CpuStats(ticks) 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/LogStreamKind.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import enumeratum._ 4 | 5 | import scala.collection.immutable 6 | 7 | /** 8 | * The kind of log stream of a client. 9 | */ 10 | sealed trait LogStreamKind extends EnumEntry with EnumEntry.Lowercase 11 | 12 | object LogStreamKind extends Enum[LogStreamKind] { 13 | 14 | override def values: immutable.IndexedSeq[LogStreamKind] = findValues 15 | 16 | /** 17 | * The standard output stream of a task. 18 | */ 19 | case object StdOut extends LogStreamKind 20 | 21 | /** 22 | * The standard error stream of a task. 23 | */ 24 | case object StdErr extends LogStreamKind 25 | } 26 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/JobStatus.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.models.JobStatus.JobStatus 4 | import play.api.libs.json._ 5 | 6 | object JobStatus extends Enumeration { 7 | 8 | type JobStatus = Value 9 | 10 | val Running = Value("running") 11 | val Pending = Value("pending") 12 | val Stopped = Value("stopped") 13 | val Dead = Value("dead") 14 | val Unknown = Value("unknown") 15 | 16 | } 17 | 18 | object JobStatusJson { 19 | 20 | implicit val jobStatusWrites: Writes[JobStatus] = Writes(value => JsString(value.toString)) 21 | 22 | implicit val jobStatusReads: Reads[JobStatus] = Reads(_.validate[String].map(JobStatus.withName)) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-create-many/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | for i in $(seq 1 500) 4 | do 5 | curl -s -H 'Content-Type: application/json' \ 6 | -X POST -d "{ \"templateId\": \"http-server\", \"parameters\": { \"id\": \"test-http-$i\", \"cpu\": 250 } }" \ 7 | 'http://localhost:9000/api/v1/instances' > /dev/null 8 | done 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | docker stop $(cat cluster-broccoli.did) 11 | sleep $BROCCOLI_SLEEP_SHORT 12 | docker run --rm -d --net host \ 13 | frosner/cluster-broccoli-test \ 14 | cluster-broccoli \ 15 | -Dconfig.file=/couchdb.conf > cluster-broccoli.did 16 | sleep $BROCCOLI_SLEEP_MEDIUM 17 | check_service http localhost 9000 18 | -------------------------------------------------------------------------------- /server/src/it/scala/de/frosner/broccoli/signal/UnixSignalManagerIntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.signal 2 | 3 | import org.specs2.mock.Mockito 4 | import org.specs2.mutable.Specification 5 | import sun.misc.{Signal, SignalHandler} 6 | 7 | class UnixSignalManagerIntegrationSpec extends Specification with Mockito { 8 | "Registering new signal" should { 9 | "trigger the handler when the signal is raised" in { 10 | val manager = new UnixSignalManager() 11 | val signal = new Signal("USR2") 12 | val handler = mock[SignalHandler] 13 | manager.register(signal, handler) 14 | Signal.raise(signal) 15 | Thread.sleep(1000) 16 | there was one(handler).handle(signal) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/templates/TemporaryTemplatesContext.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | 6 | import de.frosner.broccoli.util.TemporaryDirectoryContext 7 | import org.apache.commons.io.FileUtils 8 | import org.specs2.execute.{AsResult, Result} 9 | 10 | trait TemporaryTemplatesContext extends TemporaryDirectoryContext { 11 | override protected def foreach[R: AsResult](f: (Path) => R): Result = super.foreach { path: Path => 12 | val templatesPath = getClass.getResource("/de/frosner/broccoli/templates") 13 | FileUtils.copyDirectoryToDirectory(new File(templatesPath.getFile), path.toFile) 14 | f(path.resolve("templates")) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-delete/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | 4 | curl -H 'Content-Type: application/json' \ 5 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 6 | 'http://localhost:9000/api/v1/instances' 7 | sleep $BROCCOLI_SLEEP_SHORT 8 | curl -X DELETE 'localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | docker stop $(cat cluster-broccoli.did) 11 | sleep $BROCCOLI_SLEEP_SHORT 12 | docker run --rm -d --net host \ 13 | frosner/cluster-broccoli-test \ 14 | cluster-broccoli \ 15 | -Dconfig.file=/couchdb.conf > cluster-broccoli.did 16 | sleep $BROCCOLI_SLEEP_MEDIUM 17 | check_service http localhost 9000 18 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-create-many/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | for i in $(seq 1 500) 4 | do 5 | curl -s -H 'Content-Type: application/json' \ 6 | -X POST -d "{ \"templateId\": \"http-server\", \"parameters\": { \"id\": \"test-http-$i\", \"cpu\": 250 } }" \ 7 | 'http://localhost:9000/api/v1/instances' > /dev/null 8 | done 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | docker stop $(cat cluster-broccoli.did) 11 | sleep $BROCCOLI_SLEEP_SHORT 12 | docker run --rm -d --net host \ 13 | -v /tmp/instances:/cluster-broccoli-dist/instances \ 14 | frosner/cluster-broccoli-test \ 15 | cluster-broccoli > cluster-broccoli.did 16 | sleep $BROCCOLI_SLEEP_MEDIUM 17 | check_service http localhost 9000 18 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/BroccoliFingerprintGenerator.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import com.mohiva.play.silhouette.api.crypto.Hash 4 | import com.mohiva.play.silhouette.api.util.FingerprintGenerator 5 | import play.api.http.HeaderNames.USER_AGENT 6 | import play.api.mvc.RequestHeader 7 | 8 | case class BroccoliFingerprintGenerator(includeRemoteAddress: Boolean = false) extends FingerprintGenerator { 9 | override def generate(implicit request: RequestHeader): String = 10 | Hash.sha1( 11 | new StringBuilder() 12 | .append(request.headers.get(USER_AGENT).getOrElse("")) 13 | .append(":") 14 | .append(if (includeRemoteAddress) request.remoteAddress else "") 15 | .toString()) 16 | } 17 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-delete/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | curl -H 'Content-Type: application/json' \ 4 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 5 | 'http://localhost:9000/api/v1/instances' 6 | sleep $BROCCOLI_SLEEP_SHORT 7 | curl -X DELETE 'localhost:9000/api/v1/instances/test-http' 8 | sleep $BROCCOLI_SLEEP_SHORT 9 | docker stop $(cat cluster-broccoli.did) 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | docker run --rm -d --net host \ 12 | -v /tmp/instances:/cluster-broccoli-dist/instances \ 13 | frosner/cluster-broccoli-test \ 14 | cluster-broccoli > cluster-broccoli.did 15 | sleep $BROCCOLI_SLEEP_MEDIUM 16 | check_service http localhost 9000 17 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/routes/Extractors.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.routes 2 | 3 | import de.frosner.broccoli.nomad.models.LogStreamKind 4 | import play.api.routing.sird.PathBindableExtractor 5 | import squants.information.Information 6 | 7 | /** 8 | * Additional extractors for string DSL routes. 9 | */ 10 | trait Extractors { 11 | import PathBinders._ 12 | 13 | /** 14 | * The kind of a log. 15 | */ 16 | val logKind: PathBindableExtractor[LogStreamKind] = new PathBindableExtractor[LogStreamKind] 17 | 18 | /** 19 | * Extract units of information from paths. 20 | */ 21 | val information: PathBindableExtractor[Information] = new PathBindableExtractor[Information] 22 | } 23 | 24 | object Extractors extends Extractors 25 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/signal/SignalManager.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.signal 2 | 3 | import sun.misc.{Signal, SignalHandler} 4 | 5 | /** 6 | * Provide a way to register and unregister OS signals 7 | */ 8 | trait SignalManager { 9 | 10 | /** 11 | * Registers signal handler for the given signal. 12 | * 13 | * @param signal 14 | * @param handler 15 | * @throws IllegalArgumentException if a signal is already registered or reserved by JDK or OS 16 | * @throws UnsupportedOperationException if OS is not supported 17 | */ 18 | def register(signal: Signal, handler: SignalHandler): Unit 19 | 20 | /** 21 | * Unregisters the signal 22 | * 23 | * @param signal 24 | */ 25 | def unregister(signal: Signal): Unit 26 | } 27 | -------------------------------------------------------------------------------- /webui/src/Views/Styles.elm: -------------------------------------------------------------------------------- 1 | module Views.Styles exposing (..) 2 | 3 | 4 | instanceViewElementStyle : List ( String, String ) 5 | instanceViewElementStyle = 6 | [ ( "margin-bottom", "15px" ) ] 7 | 8 | 9 | checkboxColumnWidth : number 10 | checkboxColumnWidth = 11 | 1 12 | 13 | 14 | chevronColumnWidth : number 15 | chevronColumnWidth = 16 | 30 17 | 18 | 19 | serviceColumnWidth : number 20 | serviceColumnWidth = 21 | 500 22 | 23 | 24 | templateVersionColumnWidth : number 25 | templateVersionColumnWidth = 26 | 1 27 | 28 | 29 | jobControlsColumnWidth : number 30 | jobControlsColumnWidth = 31 | 200 32 | 33 | 34 | expandedTdStyle : List ( String, String ) 35 | expandedTdStyle = 36 | [ ( "border-top", "0px" ) 37 | , ( "padding-top", "0px" ) 38 | ] 39 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceUpdate.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.models.JobStatus.JobStatus 4 | import JobStatusJson.{jobStatusReads, jobStatusWrites} 5 | import play.api.libs.json.{JsValue, Json} 6 | 7 | case class InstanceUpdate( 8 | instanceId: Option[String], // Option because we don't need it from the HTTP API, only for the websocket 9 | status: Option[JobStatus], 10 | parameterValues: Option[Map[String, JsValue]], 11 | periodicJobsToStop: Option[List[String]], 12 | selectedTemplate: Option[String]) 13 | 14 | object InstanceUpdate { 15 | 16 | implicit val instanceUpdateWrites = Json.writes[InstanceUpdate] 17 | 18 | implicit val instanceUpdateReads = Json.reads[InstanceUpdate] 19 | 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/TaskState.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import enumeratum.{Enum, EnumEntry, PlayJsonEnum} 4 | 5 | import scala.collection.immutable 6 | 7 | /** 8 | * The state of a single task. 9 | * 10 | * See https://github.com/hashicorp/nomad/blob/2e7d8adfa4e9cbbc85009943f79641ac55875aa6/nomad/structs/structs.go#L3367 11 | * for the list of possible task states. 12 | */ 13 | sealed trait TaskState extends EnumEntry with EnumEntry.Lowercase 14 | 15 | object TaskState extends Enum[TaskState] with PlayJsonEnum[TaskState] { 16 | override val values: immutable.IndexedSeq[TaskState] = findValues 17 | 18 | case object Pending extends TaskState 19 | case object Running extends TaskState 20 | case object Dead extends TaskState 21 | } 22 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/nomad/models/NodeSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import de.frosner.broccoli.util 4 | import org.specs2.mutable.Specification 5 | import play.api.libs.json.Json 6 | 7 | class NodeSpec extends Specification { 8 | "Node" should { 9 | "decode from JSON" in { 10 | val node = Json 11 | .parse(util.Resources.readAsString("/de/frosner/broccoli/services/nomad/node.json")) 12 | .validate[Node] 13 | .asEither 14 | 15 | node should beRight( 16 | Node( 17 | id = shapeless.tag[Node.Id]("4beac5b7-3974-3ddf-b572-9db5906fb891"), 18 | name = shapeless.tag[Node.Name]("vc31"), 19 | httpAddress = shapeless.tag[Node.HttpAddress]("127.0.0.1:4646") 20 | )) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /webui/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const config = require('./webpack.config.js'); 4 | 5 | /** 6 | * Production configuration for webpack, via yarn package. Provides more control about the yarn 7 | * development settings than webpack -p. In particular we can get rid of UglifyJS which we don't 8 | * need. 9 | */ 10 | module.exports = merge(config, { 11 | plugins: [ 12 | // Force all loaders to minimize their output and disable debugging tools 13 | new webpack.LoaderOptionsPlugin({ 14 | minimize: true, 15 | debug: false 16 | }), 17 | // Switch libraries into production mode 18 | new webpack.DefinePlugin({ 19 | 'process.env': { 20 | 'NODE_ENV': JSON.stringify('production') 21 | } 22 | }) 23 | ] 24 | }); 25 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/AllocationStats.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.json.{JsPath, Reads} 4 | import play.api.libs.functional.syntax._ 5 | import shapeless.tag.@@ 6 | 7 | /** 8 | * Allocation statistics. 9 | * 10 | * @param resourceUsage Resource usage of the entire allocation 11 | * @param tasks Resource usage per task 12 | */ 13 | final case class AllocationStats(resourceUsage: ResourceUsage, tasks: Map[String @@ Task.Name, TaskStats]) 14 | 15 | object AllocationStats { 16 | implicit val allocationStatsReads: Reads[AllocationStats] = 17 | ((JsPath \ "ResourceUsage").read[ResourceUsage] and 18 | (JsPath \ "Tasks").read[Map[String, TaskStats]].map(_.asInstanceOf[Map[String @@ Task.Name, TaskStats]]))( 19 | AllocationStats.apply _) 20 | } 21 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-stop/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "status": "stopped" }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | curl -k -X PUT https://localhost:4646/v1/system/gc 15 | sleep $BROCCOLI_SLEEP_SHORT 16 | curl -k -X PUT https://localhost:4646/v1/system/gc 17 | sleep $BROCCOLI_SLEEP_SHORT 18 | 19 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/BroccoliModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import com.google.inject.{AbstractModule, Provides, Singleton} 4 | import net.codingwell.scalaguice.ScalaModule 5 | import play.api.Configuration 6 | import pureconfig._ 7 | import pureconfig.module.enumeratum._ 8 | 9 | /** 10 | * Provide basic broccoli globals. 11 | */ 12 | class BroccoliModule extends AbstractModule with ScalaModule { 13 | override def configure(): Unit = {} 14 | 15 | /** 16 | * Provide the Broccoli configuration. 17 | * 18 | * @param configuration The underlying configuration to load from. 19 | */ 20 | @Provides 21 | @Singleton 22 | def provideConfiguration(configuration: Configuration): BroccoliConfiguration = 23 | loadConfigOrThrow[BroccoliConfiguration](configuration.underlying.getConfig("broccoli")) 24 | } 25 | -------------------------------------------------------------------------------- /webui/src/Models/Ui/InstanceParameterForm.elm: -------------------------------------------------------------------------------- 1 | module Models.Ui.InstanceParameterForm exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Maybe exposing (Maybe) 5 | import Models.Resources.Template exposing (ParameterValue, Template) 6 | 7 | 8 | 9 | -- ParameterValues are kept in String here as they must be displayed in the input 10 | -- If the ParameterValue was invalid (eg: String when expectation was IntParamVal) 11 | -- we cannot make the user input go away. So we store the raw format in a String. 12 | 13 | 14 | type alias InstanceParameterForm = 15 | { originalParameterValues : Dict String (Maybe String) 16 | , changedParameterValues : Dict String (Maybe String) 17 | , selectedTemplate : Maybe Template 18 | } 19 | 20 | 21 | empty = 22 | InstanceParameterForm 23 | Dict.empty 24 | Dict.empty 25 | Nothing 26 | -------------------------------------------------------------------------------- /docker/test/cluster-broccoli-files/couchdb-tls.conf: -------------------------------------------------------------------------------- 1 | # Use couchdb for play 2 | play.crypto.secret = "yJEqZLcZQyWXfgYvhMbRqwakspWpT6oFnNLgTtmzVazrvVykQPaRWhNZDNwUZWbA" 3 | 4 | play.modules.disabled += "de.frosner.broccoli.instances.storage.filesystem.FileSystemStorageModule" 5 | play.modules.enabled += "de.frosner.broccoli.instances.storage.couchdb.CouchDBStorageModule" 6 | 7 | play.ws.ssl { 8 | trustManager = { 9 | stores = [ 10 | { type = "PEM", path = "/nomad-ca.pem" } 11 | ] 12 | } 13 | keyManager = { 14 | stores = [ 15 | { type = "JKS", path = "/broccoli.global.nomad.jks", password = "inttest" } 16 | ] 17 | } 18 | debug = { 19 | ssl = true 20 | trustmanager = true 21 | keymanager = true 22 | } 23 | } 24 | 25 | play.ws.ssl.loose.acceptAnyCertificate=true 26 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-delete/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http" } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -X DELETE 'http://localhost:9000/api/v1/instances/test-http' 11 | echo -e "\nSleeping after deleting instance" 12 | sleep $BROCCOLI_SLEEP_SHORT 13 | curl -k -X PUT 'https://localhost:4646/v1/system/gc' 14 | sleep $BROCCOLI_SLEEP_SHORT 15 | # For some reason gc does not work in the first try inside nomad 0.9.5 16 | curl -k -X PUT 'https://localhost:4646/v1/system/gc' 17 | sleep $BROCCOLI_SLEEP_SHORT 18 | 19 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-couchdb/api-v1-instances-show-after-edit/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | 4 | curl -H 'Content-Type: application/json' \ 5 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 6 | 'http://localhost:9000/api/v1/instances' 7 | sleep $BROCCOLI_SLEEP_SHORT 8 | curl -v -H 'Content-Type: application/json' \ 9 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 10 | 'http://localhost:9000/api/v1/instances/test-http' 11 | sleep $BROCCOLI_SLEEP_SHORT 12 | docker stop $(cat cluster-broccoli.did) 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | docker run --rm -d --net host \ 15 | frosner/cluster-broccoli-test \ 16 | cluster-broccoli \ 17 | -Dconfig.file=/couchdb.conf > cluster-broccoli.did 18 | sleep $BROCCOLI_SLEEP_MEDIUM 19 | check_service http localhost 9000 20 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/Account.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import com.mohiva.play.silhouette.api.Identity 4 | import play.api.libs.json.{Json, OFormat} 5 | 6 | /** 7 | * A user account in Broccoli. 8 | * 9 | * @param name The account name 10 | * @param instanceRegex A regex matching instance names this account is allowed to access 11 | * @param role The role of the user 12 | */ 13 | final case class Account(name: String, instanceRegex: String, role: Role) extends Identity { 14 | def getOEPrefix: String = name.split("-")(0) 15 | } 16 | 17 | object Account { 18 | implicit val accountFormat: OFormat[Account] = Json.format[Account] 19 | 20 | /** 21 | * The anonymous user. 22 | */ 23 | val anonymous = Account( 24 | name = "anonymous", 25 | instanceRegex = ".*", 26 | role = Role.Administrator 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceUpdated.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceUpdated exposing (InstanceUpdated, decoder) 2 | 3 | import Json.Decode as Decode 4 | import Models.Resources.Instance as Instance exposing (Instance) 5 | import Models.Resources.InstanceUpdate as InstanceUpdate exposing (InstanceUpdate) 6 | 7 | 8 | type alias InstanceUpdated = 9 | { instanceUpdate : InstanceUpdate 10 | , instance : Instance 11 | } 12 | 13 | 14 | decoder : Decode.Decoder InstanceUpdated 15 | decoder = 16 | Decode.field "instanceWithStatus" Instance.decoder 17 | |> Decode.andThen 18 | (\instanceWithStatus -> 19 | Decode.map2 InstanceUpdated 20 | (Decode.field "instanceUpdate" (InstanceUpdate.decoder instanceWithStatus.template.parameterInfos)) 21 | (Decode.succeed instanceWithStatus) 22 | ) 23 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-parameter-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -v -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | curl -H 'Content-Type: application/json' \ 15 | -X POST -d '{ "status": "running" }' \ 16 | 'http://localhost:9000/api/v1/instances/test-http' 17 | sleep $BROCCOLI_SLEEP_SHORT 18 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-parameter-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | curl -H 'Content-Type: application/json' \ 7 | -X POST -d '{ "status": "running" }' \ 8 | 'http://localhost:9000/api/v1/instances/test-http' 9 | sleep $BROCCOLI_SLEEP_SHORT 10 | curl -v -H 'Content-Type: application/json' \ 11 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 12 | 'http://localhost:9000/api/v1/instances/test-http' 13 | sleep $BROCCOLI_SLEEP_SHORT 14 | curl -H 'Content-Type: application/json' \ 15 | -X POST -d '{ "status": "running" }' \ 16 | 'http://localhost:9000/api/v1/instances/test-http' 17 | sleep $BROCCOLI_SLEEP_SHORT 18 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-edit-template-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | 7 | curl -H 'Content-Type: application/json' \ 8 | -X POST -d '{ "status": "running" }' \ 9 | 'http://localhost:9000/api/v1/instances/test' 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | 12 | curl -H 'Content-Type: application/json' \ 13 | -X POST -d '{ "selectedTemplate": "jupyter", "parameterValues": { "id": "test" } }' \ 14 | 'http://localhost:9000/api/v1/instances/test' 15 | sleep $BROCCOLI_SLEEP_SHORT 16 | 17 | curl -H 'Content-Type: application/json' \ 18 | -X POST -d '{ "status": "running" }' \ 19 | 'http://localhost:9000/api/v1/instances/test' 20 | sleep $BROCCOLI_SLEEP_SHORT 21 | -------------------------------------------------------------------------------- /http-api-tests/instance-persistence-dir/api-v1-instances-show-after-edit/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source $(dirname "$0")/../../common.sh 3 | curl -H 'Content-Type: application/json' \ 4 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test-http", "cpu": 250 } }' \ 5 | 'http://localhost:9000/api/v1/instances' 6 | sleep $BROCCOLI_SLEEP_SHORT 7 | curl -v -H 'Content-Type: application/json' \ 8 | -X POST -d '{ "parameterValues": { "id": "test-http", "cpu": 50 } }' \ 9 | 'http://localhost:9000/api/v1/instances/test-http' 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | docker stop $(cat cluster-broccoli.did) 12 | sleep $BROCCOLI_SLEEP_SHORT 13 | docker run --rm -d --net host \ 14 | -v /tmp/instances:/cluster-broccoli-dist/instances \ 15 | frosner/cluster-broccoli-test \ 16 | cluster-broccoli > cluster-broccoli.did 17 | sleep $BROCCOLI_SLEEP_MEDIUM 18 | check_service http localhost 9000 19 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/InMemoryIdentityService.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import com.mohiva.play.silhouette.api.LoginInfo 4 | import com.mohiva.play.silhouette.api.services.IdentityService 5 | 6 | import scala.concurrent.Future 7 | 8 | /** 9 | * An in-memory identity service. 10 | * 11 | * @param identities The known identities 12 | */ 13 | class InMemoryIdentityService(identities: Seq[Account]) extends IdentityService[Account] { 14 | val logins: Map[String, Account] = identities.map(account => account.name -> account).toMap 15 | 16 | /** 17 | * Find a user in the list of identities. 18 | * 19 | * @param loginInfo The login information 20 | * @return The identity if any 21 | */ 22 | override def retrieve(loginInfo: LoginInfo): Future[Option[Account]] = 23 | Future.successful(logins.get(loginInfo.providerKey)) 24 | } 25 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/InstanceModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances 2 | 3 | import javax.inject.Singleton 4 | 5 | import com.google.inject.{AbstractModule, Provides} 6 | import com.hubspot.jinjava.JinjavaConfig 7 | import de.frosner.broccoli.BroccoliConfiguration 8 | import de.frosner.broccoli.templates.TemplateRenderer 9 | import net.codingwell.scalaguice.ScalaModule 10 | 11 | /** 12 | * Provide instance storage and template rendering implementations. 13 | */ 14 | class InstanceModule extends AbstractModule with ScalaModule { 15 | override def configure(): Unit = {} 16 | 17 | /** 18 | * Provides the template renderer for instances. 19 | */ 20 | @Provides 21 | @Singleton 22 | def provideTemplateRenderer(config: BroccoliConfiguration, jinjavaConfig: JinjavaConfig): TemplateRenderer = 23 | new TemplateRenderer(jinjavaConfig) 24 | } 25 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-edit-template-restart/before: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -H 'Content-Type: application/json' \ 3 | -X POST -d '{ "templateId": "http-server", "parameters": { "id": "test", "cpu": 250 } }' \ 4 | 'http://localhost:9000/api/v1/instances' 5 | sleep $BROCCOLI_SLEEP_SHORT 6 | 7 | curl -H 'Content-Type: application/json' \ 8 | -X POST -d '{ "status": "running" }' \ 9 | 'http://localhost:9000/api/v1/instances/test' 10 | sleep $BROCCOLI_SLEEP_SHORT 11 | 12 | curl -H 'Content-Type: application/json' \ 13 | -X POST -d '{ "selectedTemplate": "jupyter", "parameterValues": { "id": "test" } }' \ 14 | 'http://localhost:9000/api/v1/instances/test' 15 | sleep $BROCCOLI_SLEEP_SHORT 16 | 17 | curl -H 'Content-Type: application/json' \ 18 | -X POST -d '{ "status": "running" }' \ 19 | 'http://localhost:9000/api/v1/instances/test' 20 | sleep $BROCCOLI_SLEEP_SHORT 21 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/TaskState.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.TaskState exposing (..) 2 | 3 | import Json.Decode exposing (Decoder, andThen, fail, string, succeed) 4 | 5 | 6 | {-| The state of a task within an allocation. 7 | -} 8 | type TaskState 9 | = TaskDead 10 | | TaskRunning 11 | | TaskPending 12 | 13 | 14 | {-| Decode a task state from JSON. 15 | -} 16 | decoder : Decoder TaskState 17 | decoder = 18 | string 19 | |> andThen 20 | (\name -> 21 | case name of 22 | "dead" -> 23 | succeed TaskDead 24 | 25 | "running" -> 26 | succeed TaskRunning 27 | 28 | "pending" -> 29 | succeed TaskPending 30 | 31 | _ -> 32 | fail ("Unknown task state " ++ name) 33 | ) 34 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/RemoveSecrets.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import simulacrum._ 4 | 5 | import scala.language.implicitConversions 6 | 7 | /** 8 | * Typeclass to remove secrets from values of a type. 9 | * 10 | * @tparam T The type containing secrets 11 | */ 12 | @typeclass trait RemoveSecrets[T] { 13 | 14 | /** 15 | * Remove secrets from a value. 16 | * 17 | * @param value The value to remove secrets from 18 | * @return The value without secrets 19 | */ 20 | def removeSecrets(value: T): T 21 | } 22 | 23 | object RemoveSecrets { 24 | 25 | /** 26 | * Create an instance of RemoveSecrets. 27 | * 28 | * @param remove The function to remove secrets from a value 29 | */ 30 | def instance[T](remove: T => T): RemoveSecrets[T] = new RemoveSecrets[T] { 31 | override def removeSecrets(value: T): T = remove(value) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceTasks.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.http.ToHTTPResult 4 | import play.api.libs.json.{Json, Writes} 5 | import play.api.mvc.Results 6 | 7 | /** 8 | * The tasks of an instance. 9 | * 10 | * @param instanceId The ID of the instance 11 | * @param allocatedTasks Allocated tasks of the instance 12 | */ 13 | final case class InstanceTasks(instanceId: String, 14 | allocatedTasks: Seq[AllocatedTask], 15 | allocatedPeriodicTasks: Map[String, Seq[AllocatedTask]]) 16 | 17 | object InstanceTasks { 18 | implicit val instanceTasksWrites: Writes[InstanceTasks] = Json.writes[InstanceTasks] 19 | 20 | implicit val instanceTasksToHTTPResult: ToHTTPResult[InstanceTasks] = 21 | ToHTTPResult.instance(v => Results.Ok(Json.toJson(v.allocatedTasks))) 22 | } 23 | -------------------------------------------------------------------------------- /prepare-docker-builds: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | rm -rf cluster-broccoli-* 6 | rm -rf docker/test/cluster-broccoli-dist 7 | sbt clean && sbt dist 8 | unzip server/target/universal/cluster-broccoli*.zip 9 | cp -R cluster-broccoli-* docker/test/cluster-broccoli-dist 10 | cp -R templates docker/test/ 11 | rm -rf cluster-broccoli-* 12 | 13 | if [ -z "$BROCCOLI_SLEEP_LONG" ]; then 14 | export BROCCOLI_SLEEP_LONG=10 15 | fi 16 | echo '$BROCCOLI_SLEEP_LONG'="$BROCCOLI_SLEEP_LONG" 17 | 18 | if [ -z "$BROCCOLI_SLEEP_MEDIUM" ]; then 19 | export BROCCOLI_SLEEP_MEDIUM=3 20 | fi 21 | echo '$BROCCOLI_SLEEP_MEDIUM'="$BROCCOLI_SLEEP_MEDIUM" 22 | 23 | if [ -z "$BROCCOLI_SLEEP_SHORT" ]; then 24 | export BROCCOLI_SLEEP_SHORT=1 25 | fi 26 | 27 | if [ -z "BROCCOLI_TIMEOUT_ATTEMPTS" ]; then 28 | export BROCCOLI_TIMEOUT_ATTEMPTS=10 29 | fi 30 | echo '$BROCCOLI_SLEEP_SHORT'="$BROCCOLI_SLEEP_SHORT" 31 | 32 | set +e 33 | 34 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/ClientStatus.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import enumeratum._ 4 | 5 | import scala.collection.immutable 6 | 7 | /** 8 | * The nomad client status. 9 | * 10 | * Presumably this is the status the allocation 11 | * 12 | * See https://github.com/hashicorp/nomad/blob/2e7d8adfa4e9cbbc85009943f79641ac55875aa6/nomad/structs/structs.go#L4260 13 | * for all possible values. 14 | */ 15 | sealed trait ClientStatus extends EnumEntry with EnumEntry.Lowercase 16 | 17 | object ClientStatus extends Enum[ClientStatus] with PlayJsonEnum[ClientStatus] { 18 | 19 | override val values: immutable.IndexedSeq[ClientStatus] = findValues 20 | 21 | case object Pending extends ClientStatus 22 | case object Running extends ClientStatus 23 | case object Complete extends ClientStatus 24 | case object Failed extends ClientStatus 25 | case object Lost extends ClientStatus 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/SignalRefreshedTemplateSource.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import de.frosner.broccoli.models.Template 4 | import de.frosner.broccoli.signal.SignalManager 5 | import sun.misc.{Signal, SignalHandler} 6 | 7 | /** 8 | * The template source that wraps CachedTemplateSource and refreshes the cache after receiving SIGUSR2 9 | * 10 | * @param source The CachedTemplateSource that will be wrapped 11 | */ 12 | class SignalRefreshedTemplateSource(source: CachedTemplateSource, signalManager: SignalManager) extends TemplateSource { 13 | 14 | override val templateRenderer: TemplateRenderer = source.templateRenderer 15 | 16 | signalManager.register(new Signal("USR2"), new SignalHandler() { 17 | def handle(sig: Signal) { 18 | source.refresh() 19 | } 20 | }) 21 | 22 | override def loadTemplates(refreshed: Boolean): Seq[Template] = source.loadTemplates(refreshed) 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/logging.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import scala.concurrent.duration.Duration 6 | 7 | /** 8 | * Utilities for Logging 9 | */ 10 | object logging { 11 | 12 | /** 13 | * Log the time it takes to execute a block. 14 | * 15 | * @param label The human-readable label describing the operation the block performs 16 | * @param block The block of code to measure 17 | * @param log A function to emit the log message 18 | * @tparam T The type of the result of the block 19 | * @return The result of running the block 20 | */ 21 | def logExecutionTime[T](label: String)(block: => T)(log: (=> String) => Unit): T = { 22 | val start = System.nanoTime() 23 | val result = block 24 | val duration = Duration(System.nanoTime() - start, TimeUnit.NANOSECONDS) 25 | log(s"$label took ${duration.toMillis} ms") 26 | result 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/it/scala/de/frosner/broccoli/test/contexts/WSClientContext.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.test.contexts 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | import org.specs2.execute.{AsResult, Result} 6 | import org.specs2.specification.ForEach 7 | import play.api.libs.ws.WSClient 8 | import play.api.libs.ws.ahc.AhcWSClient 9 | 10 | /** 11 | * Provides a WSClient instance to tests. 12 | * 13 | * Requires the ExecutionEnvironment to be mixed in. 14 | */ 15 | trait WSClientContext extends ForEach[WSClient] { 16 | 17 | override protected def foreach[R: AsResult](f: (WSClient) => R): Result = { 18 | implicit val actorSystem = ActorSystem("nomad-http-client") 19 | try { 20 | implicit val materializer = ActorMaterializer() 21 | val client: WSClient = AhcWSClient() 22 | try AsResult(f(client)) 23 | finally client.close() 24 | } finally { 25 | actorSystem.terminate() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/models/InstanceTasksSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.http.ToHTTPResult.ToToHTTPResultOps 4 | import de.frosner.broccoli.nomad 5 | import org.specs2.ScalaCheck 6 | import play.api.libs.json.Json 7 | import play.api.test.PlaySpecification 8 | 9 | import scala.concurrent.Future 10 | 11 | class InstanceTasksSpec 12 | extends PlaySpecification 13 | with ScalaCheck 14 | with ModelArbitraries 15 | with nomad.ModelArbitraries 16 | with ToToHTTPResultOps { 17 | "The ToHTTPResult instance" should { 18 | "convert in 200 OK result" in prop { (instanceTasks: InstanceTasks) => 19 | status(Future.successful(instanceTasks.toHTTPResult)) === OK 20 | } 21 | 22 | "convert the tasks to a JSON body" in prop { (instanceTasks: InstanceTasks) => 23 | contentAsJson(Future.successful(instanceTasks.toHTTPResult)) === Json.toJson(instanceTasks.allocatedTasks) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/CachedTemplateSource.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import de.frosner.broccoli.models.Template 4 | 5 | /** 6 | * The template source that wraps another template source and caches loaded templates of that source 7 | * 8 | * @param source The template source that will be cached 9 | */ 10 | class CachedTemplateSource(source: TemplateSource) extends TemplateSource { 11 | @volatile private var templatesCache: Option[Seq[Template]] = None 12 | 13 | override val templateRenderer: TemplateRenderer = source.templateRenderer 14 | 15 | override def loadTemplates(refreshed: Boolean): Seq[Template] = { 16 | if (refreshed) { 17 | templatesCache = None 18 | } 19 | templatesCache match { 20 | case Some(templates) => templates 21 | case None => 22 | refresh() 23 | templatesCache.get 24 | } 25 | } 26 | 27 | def refresh(): Unit = templatesCache = Some(source.loadTemplates) 28 | } 29 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/jinjava/JinjavaConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates.jinjava 2 | 3 | import com.hubspot.jinjava.JinjavaConfig 4 | import JinjavaConfiguration.defaultJinJavaConfig 5 | 6 | final case class JinjavaConfiguration( 7 | maxRenderDepth: Int = defaultJinJavaConfig.getMaxRenderDepth, 8 | trimBlocks: Boolean = defaultJinJavaConfig.isTrimBlocks, 9 | lstripBlocks: Boolean = defaultJinJavaConfig.isLstripBlocks, 10 | readOnlyResolver: Boolean = defaultJinJavaConfig.isReadOnlyResolver, 11 | enableRecursiveMacroCalls: Boolean = defaultJinJavaConfig.isEnableRecursiveMacroCalls, 12 | failOnUnknownTokens: Boolean = defaultJinJavaConfig.isFailOnUnknownTokens, 13 | maxOutputSize: Long = defaultJinJavaConfig.getMaxOutputSize, 14 | nestedInterpretationEnabled: Boolean = defaultJinJavaConfig.isNestedInterpretationEnabled) 15 | 16 | object JinjavaConfiguration { 17 | val defaultJinJavaConfig = new JinjavaConfig() 18 | } 19 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceCreated.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceCreated exposing (InstanceCreated, decoder) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode exposing (field) 5 | import Models.Resources.Instance as Instance exposing (Instance) 6 | import Models.Resources.InstanceCreation as InstanceCreation exposing (InstanceCreation) 7 | import Models.Resources.Template exposing (ParameterInfo) 8 | 9 | 10 | type alias InstanceCreated = 11 | { instanceCreation : InstanceCreation 12 | , instance : Instance 13 | } 14 | 15 | 16 | decoder : Decode.Decoder InstanceCreated 17 | decoder = 18 | field "instanceWithStatus" Instance.decoder 19 | |> Decode.andThen 20 | (\instanceWithStatus -> 21 | Decode.map2 InstanceCreated 22 | (field "instanceCreation" (InstanceCreation.decoder instanceWithStatus.template.parameterInfos)) 23 | (Decode.succeed instanceWithStatus) 24 | ) 25 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Node.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json.{JsPath, Reads} 5 | import shapeless.tag 6 | import shapeless.tag.@@ 7 | 8 | /** 9 | * A partial model of a Nomad node. 10 | * 11 | * @param id The Nomad ID (as UUID) 12 | * @param name The name of the node 13 | * @param httpAddress The HTTP address of the node, for client API requests 14 | */ 15 | final case class Node( 16 | id: String @@ Node.Id, 17 | name: String @@ Node.Name, 18 | httpAddress: String @@ Node.HttpAddress 19 | ) 20 | 21 | object Node { 22 | sealed trait Id 23 | sealed trait Name 24 | sealed trait HttpAddress 25 | 26 | implicit val nodeReads: Reads[Node] = ( 27 | (JsPath \ "ID").read[String].map(tag[Id](_)) and 28 | (JsPath \ "Name").read[String].map(tag[Name](_)) and 29 | (JsPath \ "HTTPAddr").read[String].map(tag[HttpAddress](_)) 30 | )(Node.apply _) 31 | } 32 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/signal/UnixSignalManagerSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.signal 2 | 3 | import org.specs2.mock.Mockito 4 | import org.specs2.mutable.Specification 5 | import sun.misc.{Signal, SignalHandler} 6 | 7 | class UnixSignalManagerSpec extends Specification with Mockito { 8 | "Registering new signal" should { 9 | "fail if the signal is reserved by the JVM" in { 10 | val manager = new UnixSignalManager() 11 | manager.register(new Signal("USR1"), mock[SignalHandler]) must throwA( 12 | new IllegalArgumentException("Signal already used by VM or OS: SIGUSR1")) 13 | } 14 | 15 | "fail if the signal is already registered" in { 16 | val manager = new UnixSignalManager() 17 | val handler = mock[SignalHandler] 18 | manager.register(new Signal("USR2"), handler) 19 | manager.register(new Signal("USR2"), handler) must throwA( 20 | new IllegalArgumentException(s"Signal ${new Signal("USR2")} is already registered")) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/nomad/models/AllocationSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import de.frosner.broccoli.util 4 | import org.specs2.mutable.Specification 5 | import play.api.libs.json.Json 6 | 7 | class AllocationSpec extends Specification { 8 | 9 | "Allocation" should { 10 | 11 | "decode from JSON" in { 12 | val allocations = Json 13 | .parse(util.Resources.readAsString("/de/frosner/broccoli/services/nomad/allocations.json")) 14 | .validate[List[Allocation]] 15 | .asEither 16 | 17 | allocations should beRight( 18 | List(Allocation( 19 | id = shapeless.tag[Allocation.Id]("520bc6c3-53c9-fd2e-5bea-7d0b9dbef254"), 20 | jobId = shapeless.tag[Job.Id]("tvftarcxrPoy9wNhghqQogihjha"), 21 | nodeId = shapeless.tag[Node.Id]("cf3338e9-5ed0-88ef-df7b-9dd9708130c8"), 22 | clientStatus = ClientStatus.Running, 23 | taskStates = Map(shapeless.tag[Task.Name]("http-task") -> TaskStateEvents(TaskState.Running)) 24 | ))) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webui/src/index.js: -------------------------------------------------------------------------------- 1 | import './favicon-300.png'; 2 | 3 | // Load all style sheets 4 | import 'font-awesome/css/font-awesome.css'; 5 | import 'bootstrap/dist/css/bootstrap.css'; 6 | import 'animate.css/animate.css'; 7 | import './index.css'; 8 | 9 | import 'jquery'; 10 | import 'bootstrap/dist/js/bootstrap'; 11 | 12 | // Import the application from ELM 13 | import * as Elm from './Main.elm'; 14 | 15 | // Export this function on window to make it available for Elm 16 | window.copy = (text) => { 17 | // I was not able to do this in Elm so I had to use the JS function here 18 | // Elm ports also don't work because at least Chrome needs a proper onclick function for execCommand('copy') to work 19 | var dummy = document.createElement("input"); 20 | document.body.appendChild(dummy); 21 | dummy.setAttribute("id", "dummy_id"); 22 | document.getElementById("dummy_id").value = text; 23 | dummy.select(); 24 | document.execCommand("copy"); 25 | document.body.removeChild(dummy); 26 | }; 27 | 28 | export const app = Elm.Main.embed(document.getElementById('main')); 29 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceDeleted.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.auth.Account 4 | import de.frosner.broccoli.http.ToHTTPResult 5 | import play.api.libs.json.{Json, Writes} 6 | import play.api.mvc.Results 7 | 8 | /** 9 | * A deleted instance. 10 | * 11 | * @param instanceId The instance ID 12 | * @param instance The last state of the instance 13 | */ 14 | final case class InstanceDeleted(instanceId: String, instance: InstanceWithStatus) 15 | 16 | object InstanceDeleted { 17 | implicit def instanceDeletedWrites(implicit account: Account): Writes[InstanceDeleted] = Json.writes[InstanceDeleted] 18 | 19 | /** 20 | * Convert an instance deleted result to an HTTP result. 21 | * 22 | * The HTTP result is 200 OK with last resource value, ie, the last known instance status, in the JSON body. 23 | */ 24 | implicit def instanceDeletedToHTTPResult(implicit account: Account): ToHTTPResult[InstanceDeleted] = 25 | ToHTTPResult.instance { value => 26 | Results.Ok(Json.toJson(value.instance)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webui/src/Views/Notifications.elm: -------------------------------------------------------------------------------- 1 | module Views.Notifications exposing (view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (onClick) 6 | import Messages exposing (..) 7 | import Models.Ui.Notifications exposing (Error, Errors) 8 | import Updates.Messages exposing (..) 9 | import Utils.HtmlUtils exposing (..) 10 | 11 | 12 | view : Errors -> Html AnyMsg 13 | view errors = 14 | let 15 | indexedErrors = 16 | List.indexedMap (,) errors 17 | in 18 | div 19 | [ class "container" ] 20 | (List.map errorAlert indexedErrors) 21 | 22 | 23 | errorAlert : ( Int, Error ) -> Html AnyMsg 24 | errorAlert ( index, error ) = 25 | div 26 | [ class "alert alert-danger animated fadeIn" ] 27 | [ button 28 | [ type_ "button" 29 | , class "close" 30 | , id (String.concat [ "close-error-", toString index ]) 31 | , onClick (UpdateErrorsMsg (CloseError index)) 32 | ] 33 | [ icon "fa fa-times" [] ] 34 | , text error 35 | ] 36 | -------------------------------------------------------------------------------- /webui/tests/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Json.Encode exposing (Value) 4 | import Test exposing (describe) 5 | import Test.Runner.Node exposing (TestProgram, run) 6 | import Utils.DictUtilsSuite as DictUtilsSuite 7 | import Utils.ParameterUtilsSuite as ParameterUtilsSuite 8 | import Views.BodySuite as BodySuite 9 | import Views.FooterSuite as FooterSuite 10 | import Views.HeaderSuite as HeaderSuite 11 | import Views.LogUrlSuite as LogUrlSuite 12 | import Views.NotificationsSuite as NotificationsSuite 13 | import Views.PeriodicRunsViewSuite as PeriodicRunsViewSuite 14 | 15 | 16 | main : TestProgram 17 | main = 18 | run 19 | emit 20 | (describe 21 | "Cluster Broccoli UI Tests" 22 | [ ParameterUtilsSuite.tests 23 | , DictUtilsSuite.tests 24 | , HeaderSuite.tests 25 | , NotificationsSuite.tests 26 | , BodySuite.tests 27 | , FooterSuite.tests 28 | , LogUrlSuite.tests 29 | , PeriodicRunsViewSuite.tests 30 | ] 31 | ) 32 | 33 | 34 | port emit : ( String, Value ) -> Cmd msg 35 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceTasks.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceTasks exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode exposing (Decoder) 5 | import Models.Resources.AllocatedTask as AllocatedTask exposing (AllocatedTask) 6 | import Models.Resources.Instance exposing (InstanceId) 7 | 8 | 9 | {-| The tasks of an instance 10 | -} 11 | type alias InstanceTasks = 12 | { instanceId : InstanceId 13 | , allocatedTasks : List AllocatedTask 14 | , allocatedPeriodicTasks : Dict String (List AllocatedTask) 15 | } 16 | 17 | 18 | empty : InstanceId -> InstanceTasks 19 | empty instanceId = 20 | { instanceId = instanceId 21 | , allocatedTasks = [] 22 | , allocatedPeriodicTasks = Dict.empty 23 | } 24 | 25 | 26 | {-| Decode tasks of an instance from JSON. 27 | -} 28 | decoder : Decoder InstanceTasks 29 | decoder = 30 | Decode.map3 InstanceTasks 31 | (Decode.field "instanceId" Decode.string) 32 | (Decode.field "allocatedTasks" (Decode.list AllocatedTask.decoder)) 33 | (Decode.field "allocatedPeriodicTasks" (Decode.dict (Decode.list AllocatedTask.decoder))) 34 | -------------------------------------------------------------------------------- /server/src/it/scala/de/frosner/broccoli/test/contexts/docker/BroccoliTestService.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.test.contexts.docker 2 | 3 | import enumeratum._ 4 | 5 | sealed trait BroccoliTestService extends EnumEntry { 6 | 7 | /** 8 | * The command to run for this service. 9 | */ 10 | def command: Seq[String] 11 | } 12 | 13 | /** 14 | * Services that the Broccoli Test image provides 15 | */ 16 | object BroccoliTestService extends Enum[BroccoliTestService] { 17 | val values = findValues 18 | 19 | case object Broccoli extends BroccoliTestService { 20 | 21 | /** 22 | * The command to run for this service. 23 | */ 24 | override def command: Seq[String] = Seq("cluster-broccoli") 25 | } 26 | 27 | case object Nomad extends BroccoliTestService { 28 | 29 | /** 30 | * The command to run for this service. 31 | */ 32 | override def command: Seq[String] = Seq("nomad") 33 | } 34 | 35 | case object Consul extends BroccoliTestService { 36 | 37 | /** 38 | * The command to run for this service. 39 | */ 40 | override def command: Seq[String] = Seq("consul") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Resources.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json.{JsPath, OFormat} 5 | import shapeless.tag 6 | import shapeless.tag.@@ 7 | import squants.information.{Information, Megabytes} 8 | import squants.time.{Frequency, Megahertz} 9 | 10 | /** 11 | * Resources a task requires. 12 | * 13 | * @param cpu The CPU share required for the task 14 | * @param memory The memory required for the task 15 | */ 16 | final case class Resources(cpu: Frequency @@ Resources.CPU, memory: Information @@ Resources.Memory) 17 | 18 | object Resources { 19 | sealed trait CPU 20 | sealed trait Memory 21 | 22 | implicit val resourcesFormat: OFormat[Resources] = ( 23 | (JsPath \ "CPU") 24 | .format[Double] 25 | .inmap[Frequency @@ CPU](mhz => tag[CPU](Megahertz(mhz)), _.toMegahertz) 26 | and 27 | (JsPath \ "MemoryMB") 28 | .format[Double] 29 | .inmap[Information @@ Memory](mb => tag[Memory](Megabytes(mb)), _.toMegabytes) 30 | )(Resources.apply, unlift(Resources.unapply)) 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/signal/UnixSignalManager.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.signal 2 | 3 | import javax.inject.Singleton 4 | 5 | import org.apache.commons.lang3.SystemUtils 6 | import sun.misc.{Signal, SignalHandler} 7 | 8 | import scala.collection.mutable 9 | 10 | @Singleton 11 | class UnixSignalManager extends SignalManager { 12 | private val signals = mutable.HashMap.empty[Signal, SignalHandler] 13 | 14 | def register(signal: Signal, handler: SignalHandler): Unit = 15 | if (SystemUtils.IS_OS_UNIX) { 16 | if (signals.contains(signal)) { 17 | throw new IllegalArgumentException(s"Signal $signal is already registered") 18 | } 19 | 20 | Signal.handle(signal, handler) 21 | signals.put(signal, handler) 22 | } else { 23 | throw new UnsupportedOperationException("Signal handling is only supported on UNIX") 24 | } 25 | 26 | def unregister(signal: Signal): Unit = 27 | if (signals.contains(signal)) { 28 | Signal.handle(signal, new SignalHandler { 29 | override def handle(signal: Signal): Unit = {} 30 | }) 31 | signals.remove(signal) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/ClientStatus.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.ClientStatus exposing (..) 2 | 3 | import Json.Decode exposing (Decoder, andThen, fail, string, succeed) 4 | 5 | 6 | {-| The status of the client of an allocation. 7 | -} 8 | type ClientStatus 9 | = ClientPending 10 | | ClientRunning 11 | | ClientComplete 12 | | ClientFailed 13 | | ClientLost 14 | 15 | 16 | {-| Decode a client status from JSON. 17 | -} 18 | decoder : Decoder ClientStatus 19 | decoder = 20 | string 21 | |> andThen 22 | (\name -> 23 | case name of 24 | "pending" -> 25 | succeed ClientPending 26 | 27 | "running" -> 28 | succeed ClientRunning 29 | 30 | "complete" -> 31 | succeed ClientComplete 32 | 33 | "failed" -> 34 | succeed ClientFailed 35 | 36 | "lost" -> 37 | succeed ClientLost 38 | 39 | _ -> 40 | fail ("Unknown client status " ++ name) 41 | ) 42 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceUpdated.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.auth.Account 4 | import de.frosner.broccoli.http.ToHTTPResult 5 | import play.api.libs.json.{Json, Writes} 6 | import play.api.mvc.Results 7 | 8 | /** 9 | * An instance was updated. 10 | * 11 | * @param instanceUpdate The update performed on the instance 12 | * @param instanceWithStatus The updated instance and its status 13 | */ 14 | final case class InstanceUpdated(instanceUpdate: InstanceUpdate, instanceWithStatus: InstanceWithStatus) 15 | 16 | object InstanceUpdated { 17 | implicit def instanceUpdatedWrites(implicit account: Account): Writes[InstanceUpdated] = Json.writes[InstanceUpdated] 18 | 19 | /** 20 | * Convert an instance update result to an HTTP result. 21 | * 22 | * The HTTP result is 200 OK with the new resource value, ie, the new instance status, in the JSON body. 23 | */ 24 | implicit def instanceUpdateToHttpResult(implicit account: Account): ToHTTPResult[InstanceUpdated] = 25 | ToHTTPResult.instance { value => 26 | Results.Ok(Json.toJson(value.instanceWithStatus)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/Role.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.Role exposing (Role(..), decoder, encoder) 2 | 3 | import Json.Decode as Decode exposing (field) 4 | import Json.Encode as Encode 5 | 6 | 7 | type Role 8 | = Administrator 9 | | Operator 10 | | User 11 | 12 | 13 | decoder : Decode.Decoder Role 14 | decoder = 15 | let 16 | stringToRole s = 17 | case s of 18 | "administrator" -> 19 | Administrator 20 | 21 | "operator" -> 22 | Operator 23 | 24 | _ -> 25 | User 26 | in 27 | Decode.andThen 28 | (\statusString -> Decode.succeed (stringToRole statusString)) 29 | Decode.string 30 | 31 | 32 | encoder : Role -> Encode.Value 33 | encoder role = 34 | let 35 | roleToString s = 36 | case s of 37 | Administrator -> 38 | "administrator" 39 | 40 | Operator -> 41 | "operator" 42 | 43 | User -> 44 | "user" 45 | in 46 | role 47 | |> roleToString 48 | |> Encode.string 49 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/InstanceCreation.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.InstanceCreation exposing (InstanceCreation, decoder, encoder) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode 5 | import Json.Encode as Encode 6 | import Models.Resources.ServiceStatus as ServiceStatus exposing (ServiceStatus) 7 | import Models.Resources.Template as Template exposing (ParameterValue, decodeValueFromInfo, encodeParamValue) 8 | 9 | 10 | type alias InstanceCreation = 11 | { templateId : String 12 | , parameters : Dict String ParameterValue 13 | } 14 | 15 | 16 | decoder parameterInfos = 17 | Decode.map2 InstanceCreation 18 | (Decode.field "templateId" Decode.string) 19 | (Decode.field "parameters" (decodeValueFromInfo parameterInfos)) 20 | 21 | 22 | encoder instanceCreation = 23 | Encode.object 24 | [ ( "templateId", Encode.string instanceCreation.templateId ) 25 | , ( "parameters", parametersToObject instanceCreation.parameters ) 26 | ] 27 | 28 | 29 | parametersToObject parameters = 30 | Encode.object 31 | (parameters 32 | |> Dict.toList 33 | |> List.map (\( k, v ) -> ( k, encodeParamValue v )) 34 | ) 35 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad-tls/api-v1-instances-service-status-unknown/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-http", 3 | "template": { 4 | "id": "http-server", 5 | "description": "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests.", 6 | "parameters": [ 7 | "cpu", 8 | "id", 9 | "secret" 10 | ], 11 | "parameterInfos": { 12 | "cpu": { 13 | "id": "cpu", 14 | "name": "CPU Shares", 15 | "default": 100, 16 | "type": { 17 | "name": "integer" 18 | } 19 | }, 20 | "secret": { 21 | "id": "secret", 22 | "name": "A Secret Parameter", 23 | "default": 123.456, 24 | "secret": true, 25 | "type": { 26 | "name": "decimal" 27 | } 28 | }, 29 | "id": { 30 | "id": "id", 31 | "type": { 32 | "name": "string" 33 | } 34 | } 35 | }, 36 | "version": "2c4c43cb791d1f4fda934c0e210d7c6d" 37 | }, 38 | "parameterValues": { 39 | "id": "test-http" 40 | }, 41 | "status": "running", 42 | "services": [], 43 | "periodicRuns": [] 44 | } 45 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-nomad/api-v1-instances-service-status-unknown/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-http", 3 | "template": { 4 | "id": "http-server", 5 | "description": "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests.", 6 | "parameters": [ 7 | "cpu", 8 | "id", 9 | "secret" 10 | ], 11 | "parameterInfos": { 12 | "cpu": { 13 | "id": "cpu", 14 | "name": "CPU Shares", 15 | "default": 100, 16 | "type": { 17 | "name": "integer" 18 | } 19 | }, 20 | "secret": { 21 | "id": "secret", 22 | "name": "A Secret Parameter", 23 | "default": 123.456, 24 | "secret": true, 25 | "type": { 26 | "name": "decimal" 27 | } 28 | }, 29 | "id": { 30 | "id": "id", 31 | "type": { 32 | "name": "string" 33 | } 34 | } 35 | }, 36 | "version": "2c4c43cb791d1f4fda934c0e210d7c6d" 37 | }, 38 | "parameterValues": { 39 | "id": "test-http" 40 | }, 41 | "status": "running", 42 | "services": [], 43 | "periodicRuns": [] 44 | } 45 | -------------------------------------------------------------------------------- /webui/src/Views/JobStatusView.elm: -------------------------------------------------------------------------------- 1 | module Views.JobStatusView exposing (view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Models.Resources.JobStatus as JobStatus exposing (..) 6 | 7 | 8 | view : String -> JobStatus -> Html msg 9 | view classes jobStatus = 10 | let 11 | ( statusLabel, statusText ) = 12 | case jobStatus of 13 | JobRunning -> 14 | ( "success", "running" ) 15 | 16 | JobPending -> 17 | ( "warning", "pending" ) 18 | 19 | JobStopped -> 20 | ( "secondary", "stopped" ) 21 | 22 | JobDead -> 23 | ( "primary", "completed" ) 24 | 25 | JobUnknown -> 26 | ( "warning", "unknown" ) 27 | in 28 | span 29 | [ class (String.concat [ classes, " mr-1 pt-1 badge badge-", statusLabel ]) 30 | , style 31 | [ ( "font-size", "90%" ) 32 | , ( "width", "6rem" ) 33 | , ( "display", "inline-block" ) 34 | , ( "height", "1.5rem" ) 35 | , ( "margin-right", "8px" ) 36 | ] 37 | ] 38 | [ text statusText ] 39 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-edit/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-http", 3 | "template": { 4 | "id": "http-server", 5 | "description": "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests.", 6 | "parameters": [ 7 | "cpu", 8 | "id", 9 | "secret" 10 | ], 11 | "parameterInfos": { 12 | "cpu": { 13 | "id": "cpu", 14 | "name": "CPU Shares", 15 | "default": 100, 16 | "type": { 17 | "name": "integer" 18 | } 19 | }, 20 | "secret": { 21 | "id": "secret", 22 | "name": "A Secret Parameter", 23 | "default": 123.456, 24 | "secret": true, 25 | "type": { 26 | "name": "decimal" 27 | } 28 | }, 29 | "id": { 30 | "id": "id", 31 | "type": { 32 | "name": "string" 33 | } 34 | } 35 | }, 36 | "version": "2c4c43cb791d1f4fda934c0e210d7c6d" 37 | }, 38 | "parameterValues": { 39 | "id": "test-http", 40 | "cpu": 50 41 | }, 42 | "status": "unknown", 43 | "services": [], 44 | "periodicRuns": [] 45 | } 46 | -------------------------------------------------------------------------------- /webui/src/Updates/UpdateLoginForm.elm: -------------------------------------------------------------------------------- 1 | module Updates.UpdateLoginForm exposing (updateLoginForm) 2 | 3 | import Commands.LoginLogout 4 | import Messages exposing (AnyMsg(..)) 5 | import Models.Ui.LoginForm exposing (LoginForm) 6 | import Updates.Messages exposing (UpdateLoginFormMsg(..)) 7 | 8 | 9 | updateLoginForm : UpdateLoginFormMsg -> LoginForm -> ( LoginForm, Cmd AnyMsg ) 10 | updateLoginForm message oldLoginForm = 11 | case message of 12 | LoginAttempt username password -> 13 | ( { oldLoginForm 14 | | loginIncorrect = False 15 | } 16 | , Cmd.map UpdateLoginStatusMsg 17 | (Commands.LoginLogout.loginRequest username password) 18 | ) 19 | 20 | LogoutAttempt -> 21 | ( oldLoginForm 22 | , Cmd.map UpdateLoginStatusMsg Commands.LoginLogout.logoutRequest 23 | ) 24 | 25 | EnterUserName newUsername -> 26 | ( { oldLoginForm | username = newUsername, loginIncorrect = False } 27 | , Cmd.none 28 | ) 29 | 30 | EnterPassword newPassword -> 31 | ( { oldLoginForm | password = newPassword, loginIncorrect = False } 32 | , Cmd.none 33 | ) 34 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-edit-parameters-200/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-http", 3 | "template": { 4 | "id": "http-server", 5 | "description": "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests.", 6 | "parameters": [ 7 | "cpu", 8 | "id", 9 | "secret" 10 | ], 11 | "parameterInfos": { 12 | "cpu": { 13 | "id": "cpu", 14 | "name": "CPU Shares", 15 | "default": 100, 16 | "type": { 17 | "name": "integer" 18 | } 19 | }, 20 | "secret": { 21 | "id": "secret", 22 | "name": "A Secret Parameter", 23 | "default": 123.456, 24 | "secret": true, 25 | "type": { 26 | "name": "decimal" 27 | } 28 | }, 29 | "id": { 30 | "id": "id", 31 | "type": { 32 | "name": "string" 33 | } 34 | } 35 | }, 36 | "version": "2c4c43cb791d1f4fda934c0e210d7c6d" 37 | }, 38 | "parameterValues": { 39 | "id": "test-http", 40 | "cpu": 50 41 | }, 42 | "status": "unknown", 43 | "services": [], 44 | "periodicRuns":[] 45 | } 46 | -------------------------------------------------------------------------------- /http-api-tests/broccoli-only/api-v1-instances-show-after-create/expected/response-data: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-http", 3 | "template": { 4 | "id": "http-server", 5 | "description": "A simple Python HTTP request handler. This class serves files from the current directory and below, directly mapping the directory structure to HTTP requests.", 6 | "parameters": [ 7 | "cpu", 8 | "id", 9 | "secret" 10 | ], 11 | "parameterInfos": { 12 | "cpu": { 13 | "id": "cpu", 14 | "name": "CPU Shares", 15 | "default": 100, 16 | "type": { 17 | "name": "integer" 18 | } 19 | }, 20 | "secret": { 21 | "id": "secret", 22 | "name": "A Secret Parameter", 23 | "default": 123.456, 24 | "secret": true, 25 | "type": { 26 | "name": "decimal" 27 | } 28 | }, 29 | "id": { 30 | "id": "id", 31 | "type": { 32 | "name": "string" 33 | } 34 | } 35 | }, 36 | "version": "2c4c43cb791d1f4fda934c0e210d7c6d" 37 | }, 38 | "parameterValues": { 39 | "id": "test-http", 40 | "cpu": 250 41 | }, 42 | "status": "unknown", 43 | "services": [], 44 | "periodicRuns":[] 45 | } 46 | -------------------------------------------------------------------------------- /webui/src/Models/Resources/JobStatus.elm: -------------------------------------------------------------------------------- 1 | module Models.Resources.JobStatus exposing (JobStatus(..), decoder, encoder) 2 | 3 | import Json.Decode as Decode 4 | import Json.Encode as Encode 5 | 6 | 7 | type JobStatus 8 | = JobRunning 9 | | JobPending 10 | | JobStopped 11 | | JobDead 12 | | JobUnknown 13 | 14 | 15 | decoder = 16 | Decode.andThen 17 | (\statusString -> Decode.succeed (stringToJobStatus statusString)) 18 | Decode.string 19 | 20 | 21 | encoder jobStatus = 22 | jobStatus 23 | |> jobStatusToString 24 | |> Encode.string 25 | 26 | 27 | stringToJobStatus s = 28 | case s of 29 | "running" -> 30 | JobRunning 31 | 32 | "pending" -> 33 | JobPending 34 | 35 | "stopped" -> 36 | JobStopped 37 | 38 | "dead" -> 39 | JobDead 40 | 41 | _ -> 42 | JobUnknown 43 | 44 | 45 | jobStatusToString s = 46 | case s of 47 | JobRunning -> 48 | "running" 49 | 50 | JobPending -> 51 | "pending" 52 | 53 | JobStopped -> 54 | "stopped" 55 | 56 | JobDead -> 57 | "dead" 58 | 59 | JobUnknown -> 60 | "unknown" 61 | -------------------------------------------------------------------------------- /webui/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "sohaibiftikhar/elm-human-readable-filesize": "1.1.0 <= v < 1.1.1", 12 | "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 15 | "elm-lang/http": "1.0.0 <= v <= 2.0.0", 16 | "elm-lang/navigation": "2.1.0 <= v < 3.0.0", 17 | "evancz/url-parser": "2.0.1 <= v < 3.0.0", 18 | "myrho/elm-round": "1.0.2 <= v < 1.0.3", 19 | "panosoft/elm-websocket-client": "3.0.2 <= v <= 3.0.2", 20 | "rluiten/elm-date-extra": "9.2.0 <= v < 10.0.0", 21 | "sohaibiftikhar/elm-bootstrap": "4.1.0 <= v < 5.0.0" 22 | }, 23 | "dependency-sources": { 24 | "panosoft/elm-websocket-client": { 25 | "url": "https://github.com/panosoft/elm-websocket-client", 26 | "ref": "3.0.2" 27 | } 28 | }, 29 | "elm-version": "0.18.0 <= v < 0.19.0" 30 | } 31 | -------------------------------------------------------------------------------- /webui/src/Utils/StringUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.StringUtils exposing (..) 2 | 3 | import Http 4 | 5 | 6 | errorToString : String -> Http.Error -> String 7 | errorToString prefix error = 8 | case error of 9 | Http.BadStatus request -> 10 | String.concat 11 | [ prefix 12 | , ": " 13 | , toString request.status.code 14 | , " (" 15 | , request.status.message 16 | , ")" 17 | ] 18 | 19 | _ -> 20 | toString error 21 | 22 | 23 | {-| Surround a string with another string. 24 | surround "bar" "foo" == "barfoobar" 25 | -} 26 | surround : String -> String -> String 27 | surround wrap string = 28 | wrap ++ string ++ wrap 29 | 30 | 31 | {-| Remove surrounding strings from another string. 32 | unsurround "foo" "foobarfoo" == "bar" 33 | -} 34 | unsurround : String -> String -> String 35 | unsurround wrap string = 36 | if String.startsWith wrap string && String.endsWith wrap string then 37 | let 38 | length = 39 | String.length wrap 40 | in 41 | string 42 | |> String.dropLeft length 43 | |> String.dropRight length 44 | 45 | else 46 | string 47 | -------------------------------------------------------------------------------- /webui/src/Utils/HtmlUtils.elm: -------------------------------------------------------------------------------- 1 | module Utils.HtmlUtils exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | 6 | 7 | icon clazz attributes = 8 | span 9 | (List.append 10 | [ class clazz 11 | , attribute "aria-hidden" "true" 12 | ] 13 | attributes 14 | ) 15 | [] 16 | 17 | 18 | iconButtonText btnClass iconClass buttonText attributes = 19 | button 20 | (List.append 21 | attributes 22 | [ class btnClass 23 | , title buttonText 24 | 25 | -- , type_ "button" -- TODO should this be a button or whaz? if so, we can't use it to submit forms, can we? 26 | ] 27 | ) 28 | [ icon iconClass [] 29 | , span 30 | [ class "hidden-xs" 31 | , style [ ( "margin-left", "4px" ) ] 32 | ] 33 | [ text buttonText ] 34 | ] 35 | 36 | 37 | iconButton btnClass iconClass buttonTitle attributes = 38 | button 39 | (List.append 40 | [ class btnClass 41 | , style [ ( "padding", "0.2rem" ) ] 42 | , title buttonTitle 43 | ] 44 | attributes 45 | ) 46 | [ icon iconClass [] ] 47 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/jinjava/JinjavaModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates.jinjava 2 | 3 | import javax.inject.Singleton 4 | 5 | import com.google.inject.{AbstractModule, Provides} 6 | import com.hubspot.jinjava.JinjavaConfig 7 | import de.frosner.broccoli.BroccoliConfiguration 8 | import net.codingwell.scalaguice.ScalaModule 9 | 10 | /** 11 | * Provide JinjavaConfig for the template renderer 12 | */ 13 | class JinjavaModule extends AbstractModule with ScalaModule { 14 | override def configure(): Unit = {} 15 | @Provides 16 | @Singleton 17 | def provideJinjavaConfig(broccoliConfiguration: BroccoliConfiguration): JinjavaConfig = { 18 | val config = broccoliConfiguration.templates.jinjava 19 | JinjavaConfig 20 | .newBuilder() 21 | .withMaxRenderDepth(config.maxRenderDepth) 22 | .withTrimBlocks(config.trimBlocks) 23 | .withLstripBlocks(config.lstripBlocks) 24 | .withEnableRecursiveMacroCalls(config.enableRecursiveMacroCalls) 25 | .withReadOnlyResolver(config.readOnlyResolver) 26 | .withMaxOutputSize(config.maxOutputSize) 27 | .withNestedInterpretationEnabled(config.nestedInterpretationEnabled) 28 | .withFailOnUnknownTokens(config.failOnUnknownTokens) 29 | .build() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/http/AccessControlFilter.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.http 2 | 3 | import javax.inject.Inject 4 | 5 | import akka.stream.Materializer 6 | import play.api.http.HeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS 7 | import play.api.mvc.{Filter, RequestHeader, Result} 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | 11 | /** 12 | * Add Broccoli access control headers to responses. 13 | * 14 | * Add the following headers to responses: 15 | * 16 | * - Access-Control-Allow-Credentials: true to expose responses to the webpage 17 | * 18 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 19 | */ 20 | class AccessControlFilter @Inject()(implicit val mat: Materializer, ec: ExecutionContext) extends Filter { 21 | 22 | /** 23 | * Add access control headers to the response. 24 | * 25 | * @param next The next filter, to turn a header into a result 26 | * @param request The incoming request 27 | * @return The result of the request with access control headers added. 28 | */ 29 | override def apply(next: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = 30 | next(request).map(_.withHeaders(ACCESS_CONTROL_ALLOW_CREDENTIALS -> "true")) 31 | } 32 | -------------------------------------------------------------------------------- /docker/test/cluster-broccoli-files/application-tls.conf: -------------------------------------------------------------------------------- 1 | # Secret key 2 | # ~~~~~ 3 | # The secret key is used to secure cryptographics functions. 4 | # If you deploy your application to several instances be sure to use the same key! 5 | play.crypto.secret = "IN_PRODUCTION_CHANGE_THIS_TO_A_LONG_RANDOM_STRING" 6 | play.ws.ssl { 7 | trustManager = { 8 | stores = [ 9 | { type = "PEM", path = "/nomad-ca.pem" } 10 | ] 11 | } 12 | keyManager = { 13 | stores = [ 14 | { type = "JKS", path = "/broccoli.global.nomad.jks", password = "inttest" } 15 | ] 16 | } 17 | debug = { 18 | ssl = true 19 | trustmanager = true 20 | keymanager = true 21 | } 22 | } 23 | 24 | play.ws.ssl.loose.acceptAnyCertificate=true 25 | # Auth settings 26 | # ~~~~~ 27 | broccoli { 28 | auth { 29 | mode = none 30 | conf { 31 | accounts = [ 32 | {username:admin, password:admin, instance-regex=".*", role:"administrator"}, 33 | {username:operator, password:operator, instance-regex=".*", role:"operator"}, 34 | {username:user, password:user, instance-regex=".*", role:"user"}, 35 | {username:test, password:test, instance-regex="^test.*", role:"administrator"} 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/models/Allocation.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad.models 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json.{JsPath, Reads} 5 | import shapeless.tag 6 | import shapeless.tag.@@ 7 | 8 | /** 9 | * A partial model for a single allocations. 10 | */ 11 | final case class Allocation( 12 | id: String @@ Allocation.Id, 13 | jobId: String @@ Job.Id, 14 | nodeId: String @@ Node.Id, 15 | clientStatus: ClientStatus, 16 | taskStates: Map[String @@ Task.Name, TaskStateEvents] 17 | ) 18 | 19 | object Allocation { 20 | trait Id 21 | 22 | implicit val allocationReads: Reads[Allocation] = 23 | ((JsPath \ "ID").read[String].map(tag[Allocation.Id](_)) and 24 | (JsPath \ "JobID").read[String].map(tag[Job.Id](_)) and 25 | (JsPath \ "NodeID").read[String].map(tag[Node.Id](_)) and 26 | (JsPath \ "ClientStatus").read[ClientStatus] and 27 | (JsPath \ "TaskStates") 28 | .readNullable[Map[String, TaskStateEvents]] 29 | // Tag all values as task name. Since Task.Name is a phantom type this is a safe thing to do, albeit it doesn't 30 | // look like so 31 | .map(_.getOrElse(Map.empty).asInstanceOf[Map[String @@ Task.Name, TaskStateEvents]]))(Allocation.apply _) 32 | } 33 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/services/AboutInfoService.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import de.frosner.broccoli.auth.{Account, AuthMode} 6 | import de.frosner.broccoli.models._ 7 | 8 | @Singleton 9 | class AboutInfoService @Inject()(instanceService: InstanceService, securityService: SecurityService) { 10 | 11 | def aboutInfo(loggedIn: Account) = AboutInfo( 12 | project = AboutProject( 13 | name = de.frosner.broccoli.build.BuildInfo.name, 14 | version = de.frosner.broccoli.build.BuildInfo.version 15 | ), 16 | scala = AboutScala( 17 | version = de.frosner.broccoli.build.BuildInfo.scalaVersion 18 | ), 19 | sbt = AboutSbt( 20 | version = de.frosner.broccoli.build.BuildInfo.sbtVersion 21 | ), 22 | auth = AboutAuth( 23 | enabled = securityService.authMode != AuthMode.None, 24 | user = AboutUser( 25 | name = loggedIn.name, 26 | role = loggedIn.role, 27 | instanceRegex = loggedIn.instanceRegex 28 | ) 29 | ), 30 | services = AboutServices( 31 | clusterManager = AboutClusterManager(connected = instanceService.isNomadReachable), 32 | serviceDiscovery = AboutServiceDiscovery(connected = instanceService.isConsulReachable) 33 | ) 34 | ) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/templates/CachedTemplateSourceSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import org.specs2.mutable.Specification 4 | import org.mockito.Mockito._ 5 | import org.specs2.mock.Mockito 6 | 7 | class CachedTemplateSourceSpec extends Specification with Mockito { 8 | "Loading templates from cache " should { 9 | "load the templates from the underlying source into cache on the first run" in { 10 | val testTemplateSource = mock[TemplateSource] 11 | val cachedTemplateSource = new CachedTemplateSource(testTemplateSource) 12 | 13 | verify(testTemplateSource, times(0)).loadTemplates() 14 | val templates = cachedTemplateSource.loadTemplates() 15 | 16 | verify(testTemplateSource, times(1)).loadTemplates() 17 | templates must beEqualTo(testTemplateSource.loadTemplates()) 18 | } 19 | 20 | "return cached results on subsequent runs" in { 21 | val testTemplateSource = mock[TemplateSource] 22 | val cachedTemplateSource = new CachedTemplateSource(testTemplateSource) 23 | 24 | val templates1 = cachedTemplateSource.loadTemplates() 25 | val templates2 = cachedTemplateSource.loadTemplates() 26 | 27 | verify(testTemplateSource, times(1)).loadTemplates() 28 | templates1 must beEqualTo(templates2) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/routes/DownloadsRouter.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.routes 2 | 3 | import javax.inject.{Inject, Provider} 4 | 5 | import de.frosner.broccoli.controllers.InstanceController 6 | import de.frosner.broccoli.routes.Extractors._ 7 | import play.api.mvc.{Action, Results} 8 | import play.api.routing.Router.Routes 9 | import play.api.routing.SimpleRouter 10 | import play.api.routing.sird._ 11 | import play.api.http.ContentTypes._ 12 | 13 | class DownloadsRouter @Inject()(instances: Provider[InstanceController]) extends SimpleRouter { 14 | 15 | override def routes: Routes = { 16 | case GET( 17 | p"/instances/$instanceId/allocations/$allocationId/tasks/$taskName/logs/${logKind(kind)}" ? q_o"offset=${information(offset)}") => 18 | instances.get.logFile(instanceId, allocationId, taskName, kind, offset) 19 | case GET( 20 | p"/instances/$instanceId/periodic/$periodicJobPrefix/$periodicJobSuffix/allocations/$allocationId/tasks/$taskName/logs/${logKind( 21 | kind)}" ? q_o"offset=${information(offset)}") => 22 | val periodicJobId = s"$periodicJobPrefix/$periodicJobSuffix" 23 | instances.get.logFile(instanceId, periodicJobId, allocationId, taskName, kind, offset) 24 | case _ => Action(Results.NotFound(

Download not found

).as(HTML)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker/test/cluster-broccoli-files/nomad-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXjCCAkagAwIBAgIUSaB5ymZv68pel2V5oN9qrcUp+t4wDQYJKoZIhvcNAQEL 3 | BQAwIzEhMB8GA1UEAxMYaW50ZWdyYXRpb24tdGVzdC1yb290LWNhMB4XDTE5MDIx 4 | MTE1NDIzNloXDTI5MDIwODE1NDMwNlowIzEhMB8GA1UEAxMYaW50ZWdyYXRpb24t 5 | dGVzdC1yb290LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtd+O 6 | 3hPsQVasUbOH8Llcon7S6qbCdtgN4ao/I3VhNhwCEPPaSP8D5k9djMmUQptWy1SH 7 | jkw8DzwL275S3N7/HiTdL9xc26ZKNHiRtUSYVALXHJoyOQU6DCTSk9Ll6axk5HPp 8 | xlbuvTsbxpcCtiHplcBNr/3zfNM3Bd93uxnIAYtOYgTjjQUMi0OHUIkD9jvCpb7G 9 | H80s8WmfZMYTCrz76OMpZqBx2WzAq7xoAJhReUzAxtcXZETgj8gYbci+1K65ptLL 10 | of/yl4iRJxVXHtnAB3k4hCKvS6WcFddJux14mIjbsW05VSyof0QPaHxgvY+DhrqW 11 | pll+qTf+NIBD4Ll5ZwIDAQABo4GJMIGGMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB 12 | Af8EBTADAQH/MB0GA1UdDgQWBBRcDy1RCrzccZkfalZqXu0nKMxqUzAfBgNVHSME 13 | GDAWgBRcDy1RCrzccZkfalZqXu0nKMxqUzAjBgNVHREEHDAaghhpbnRlZ3JhdGlv 14 | bi10ZXN0LXJvb3QtY2EwDQYJKoZIhvcNAQELBQADggEBAHp3Q7B5smPKs0k5r/S4 15 | /VihmK11sSG7ncBtQZke20OVvn7g3JB1pUJoLITHKOx7VMvZygTtorthhmxpjRVa 16 | N1eg/nH0DFvdcOMlB0bH2Swi4mfRCs5gE5oowh7zXtq8jjxK78Ok3mu84k/m2H08 17 | jBQEmnsU5m/bM+L1QX4fMyW4OoUBGSyJkz9d+6eoMBBbSRJNWJEDIw4NtQXvb5Xq 18 | f24g8ZpX4AmJARcQC1+58lrL2aVWv9G5x4h/YJ8eSlseMogtBL6u+AqeyJ7xZw1k 19 | uTe+fdMARA1qQOTGvth1f2ayCJPFRKVMKmqrgknOjYJodjKxnAV6RmDpUBoS3H3u 20 | 5s8= 21 | -----END CERTIFICATE----- 22 | 23 | -------------------------------------------------------------------------------- /webui/src/Commands/LoginLogout.elm: -------------------------------------------------------------------------------- 1 | module Commands.LoginLogout exposing (loginRequest, logoutRequest, verifyLogin) 2 | 3 | import Commands.Fetch exposing (apiBaseUrl) 4 | import Http 5 | import Json.Decode 6 | import Models.Resources.UserInfo exposing (userInfoDecoder) 7 | import Updates.Messages exposing (UpdateLoginStatusMsg(..)) 8 | 9 | 10 | authBaseUrl = 11 | String.concat [ apiBaseUrl, "/auth" ] 12 | 13 | 14 | loginUrl = 15 | String.concat [ authBaseUrl, "/login" ] 16 | 17 | 18 | logoutUrl = 19 | String.concat [ authBaseUrl, "/logout" ] 20 | 21 | 22 | verifyUrl = 23 | String.concat [ authBaseUrl, "/verify" ] 24 | 25 | 26 | requestBody username password = 27 | Http.multipartBody 28 | [ Http.stringPart "username" username 29 | , Http.stringPart "password" password 30 | ] 31 | 32 | 33 | loginRequest : String -> String -> Cmd UpdateLoginStatusMsg 34 | loginRequest username password = 35 | Http.post loginUrl (requestBody username password) userInfoDecoder 36 | |> Http.send FetchLogin 37 | 38 | 39 | logoutRequest : Cmd UpdateLoginStatusMsg 40 | logoutRequest = 41 | Http.post logoutUrl Http.emptyBody Json.Decode.string 42 | |> Http.send FetchLogout 43 | 44 | 45 | verifyLogin : Cmd UpdateLoginStatusMsg 46 | verifyLogin = 47 | Http.getString verifyUrl 48 | |> Http.send FetchVerify 49 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/controllers/AboutController.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.controllers 2 | 3 | import com.mohiva.play.silhouette.api.Silhouette 4 | import javax.inject.Inject 5 | import de.frosner.broccoli.auth.{Account, BroccoliSimpleAuthorization, DefaultEnv} 6 | import de.frosner.broccoli.services._ 7 | import play.api.Environment 8 | import play.api.cache.SyncCacheApi 9 | import play.api.libs.json.Json 10 | import play.api.mvc.{BaseController, ControllerComponents} 11 | 12 | import scala.concurrent.ExecutionContext 13 | 14 | case class AboutController @Inject()( 15 | aboutInfoService: AboutInfoService, 16 | override val securityService: SecurityService, 17 | override val cacheApi: SyncCacheApi, 18 | override val playEnv: Environment, 19 | override val silhouette: Silhouette[DefaultEnv], 20 | override val controllerComponents: ControllerComponents, 21 | override val executionContext: ExecutionContext 22 | ) extends BaseController 23 | with BroccoliSimpleAuthorization { 24 | 25 | def about = Action.async(parse.empty) { implicit request => 26 | loggedIn { user => 27 | Ok(Json.toJson(AboutController.about(aboutInfoService, user))) 28 | } 29 | } 30 | } 31 | 32 | object AboutController { 33 | 34 | def about(aboutInfoService: AboutInfoService, loggedIn: Account) = 35 | aboutInfoService.aboutInfo(loggedIn) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/http/ToHTTPResultSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.http 2 | 3 | import cats.syntax.either._ 4 | import org.scalacheck.Gen 5 | import org.specs2.ScalaCheck 6 | import play.api.mvc.Results 7 | import play.api.test.PlaySpecification 8 | 9 | import scala.concurrent.Future 10 | 11 | class ToHTTPResultSpec extends PlaySpecification with ScalaCheck { 12 | import ToHTTPResult.ops._ 13 | 14 | "ToHTTPResult.instance" should { 15 | "convert to a result with the given function" in prop { (body: String, statusCode: Int) => 16 | implicit val instance = ToHTTPResult.instance[String](Results.Status(statusCode)(_)) 17 | 18 | val result = Future.successful(body.toHTTPResult) 19 | (status(result) === statusCode) and (contentAsString(result) === body) 20 | }.setGens(Gen.identifier.label("value"), Gen.choose(200, 500).label("status")) 21 | } 22 | 23 | "ToHTTPResult Either instance" should { 24 | "convert to a result of either left or right" in prop { (value: Either[String, String]) => 25 | implicit val stringToHTTPResult = ToHTTPResult.instance[String](Results.Ok(_)) 26 | val result = Future.successful(value.toHTTPResult) 27 | contentAsString(result) === value.merge 28 | }.setGen( 29 | Gen.oneOf(Gen.identifier.map(Either.left).label("left"), Gen.identifier.map(Either.right).label("right")) 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webui/tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Sample Elm Test", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "sohaibiftikhar/elm-human-readable-filesize": "1.1.0 <= v < 1.1.1", 13 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0", 14 | "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", 15 | "mgold/elm-random-pcg": "4.0.2 <= v < 5.0.0", 16 | "eeue56/elm-html-test": "3.0.0 <= v <= 3.0.0", 17 | "eeue56/elm-html-in-elm": "5.0.0 <= v <= 5.0.0", 18 | "elm-community/elm-test": "3.0.0 <= v < 4.0.0", 19 | "rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0", 20 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 21 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 22 | "elm-lang/http": "1.0.0 <= v <= 2.0.0", 23 | "elm-lang/navigation": "2.1.0 <= v < 3.0.0", 24 | "evancz/url-parser": "2.0.1 <= v < 3.0.0", 25 | "myrho/elm-round": "1.0.2 <= v < 1.0.3", 26 | "panosoft/elm-websocket-client": "3.0.2 <= v <= 3.0.2", 27 | "rluiten/elm-date-extra": "9.2.0 <= v < 10.0.0", 28 | "sohaibiftikhar/elm-bootstrap": "4.1.0 <= v < 5.0.0" 29 | }, 30 | "elm-version": "0.18.0 <= v < 0.19.0" 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/models/InstanceCreated.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.models 2 | 3 | import de.frosner.broccoli.auth.Account 4 | import de.frosner.broccoli.http.ToHTTPResult 5 | import play.api.libs.json.{Json, Writes} 6 | import play.api.mvc.Results 7 | import play.mvc.Http.HeaderNames 8 | 9 | /** 10 | * An instance was created. 11 | * 12 | * @param instanceCreation Instance creation parameters 13 | * @param instanceWithStatus The new instance and its status 14 | */ 15 | final case class InstanceCreated(instanceCreation: InstanceCreation, instanceWithStatus: InstanceWithStatus) 16 | 17 | object InstanceCreated { 18 | implicit def instanceCreatedWrites(implicit account: Account): Writes[InstanceCreated] = Json.writes[InstanceCreated] 19 | 20 | /** 21 | * Convert an instance deleted result to an HTTP result. 22 | * 23 | * The HTTP result is 201 Created with the new resource value, ie, the status of the new instance, in the JSON body, 24 | * and a Location header with the HTTP resource URL of the new instance. 25 | */ 26 | implicit def instanceCreatedToHTTPResult(implicit account: Account): ToHTTPResult[InstanceCreated] = 27 | ToHTTPResult.instance { value => 28 | Results 29 | .Created(Json.toJson(value.instanceWithStatus)) 30 | .withHeaders(HeaderNames.LOCATION -> s"/api/v1/instances/${value.instanceWithStatus.instance.id}") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webui/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | font-size: 0.9rem; 5 | } 6 | 7 | body { 8 | /* Margin bottom by footer height */ 9 | margin-bottom: 30px; 10 | padding-bottom: 29px; 11 | } 12 | 13 | code { 14 | color: #c7254e !important; 15 | } 16 | 17 | .footer { 18 | position: absolute; 19 | bottom: 0; 20 | width: 100%; 21 | /* Set the fixed height of the footer here */ 22 | height: 30px; 23 | background-color: #f5f5f5; 24 | border-top: 1px solid #ddd; 25 | } 26 | 27 | .container .text-muted { 28 | margin: 5px 0; 29 | } 30 | 31 | button:disabled { 32 | cursor: not-allowed; 33 | } 34 | 35 | .btn-group-xs > .btn, .btn-xs { 36 | padding : .25rem .4rem; 37 | font-size : .875rem; 38 | line-height : .5; 39 | border-radius : .2rem; 40 | } 41 | 42 | .vertical-align { 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .btn-no-pad { 48 | padding: 0; 49 | } 50 | 51 | .th-no-bold { 52 | font-weight: lighter; 53 | } 54 | 55 | .my-table { 56 | border-spacing: 2px; 57 | border-top: 1px solid #dee2e6; 58 | } 59 | 60 | .my-table-head { 61 | font-weight: lighter; 62 | font-size: 110%; 63 | } 64 | 65 | .my-odd-row { 66 | 67 | } 68 | 69 | .my-even-row { 70 | background-color: rgba(0, 0, 0, 0.05) 71 | } 72 | 73 | .sub-heading { 74 | font-weight: lighter; 75 | font-size: 90%; 76 | } -------------------------------------------------------------------------------- /script/instances-0.6.0-to-0.7.0.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: instances-0.6.0-to-0.7.0.sh " 5 | exit 1 6 | fi 7 | 8 | set -euo pipefail 9 | 10 | jq_version="$(jq --version 2>&1)" # redirecting stderr https://github.com/stedolan/jq/issues/1452 11 | if [[ $jq_version != *"1.5"* ]]; then 12 | echo "ERROR: This script was tested against jq-1.5 and should be run against that as well." 13 | else 14 | instanceDir="$1" 15 | backupDir="$instanceDir.bak_$(date +%s)" 16 | 17 | echo "Backing up $instanceDir to $backupDir." 18 | cp -r "$instanceDir" "$backupDir" 19 | echo "Converting instances format in $instanceDir from Broccoli <0.6.0 to 0.7.0." 20 | for instanceFile in "$instanceDir"/*.json; do 21 | echo "- Converting $instanceFile" 22 | instanceFileName=$(basename "$instanceFile") 23 | tmpInstanceFile=$(mktemp -t "$instanceFileName") 24 | jq '.template.parameterInfos = (.template.parameterInfos | with_entries(.value.id = .value.name))' < "$instanceFile" > "$tmpInstanceFile" 25 | mv "$tmpInstanceFile" "$instanceFile" 26 | done 27 | 28 | echo "Conversion finished. Looks like everything went well." 29 | read -p "Delete $backupDir? [y/n] " -n 1 -r 30 | echo 31 | if [[ $REPLY =~ ^[Yy]$ ]]; then 32 | echo "Deleting $backupDir." 33 | rm -rf "$backupDir" 34 | else 35 | echo "Keeping $backupDir for now. You have to delete it manually." 36 | fi 37 | fi 38 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/instances/storage/filesystem/FileSystemStorageModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.instances.storage.filesystem 2 | 3 | import com.google.inject.{AbstractModule, Provides, Singleton} 4 | import de.frosner.broccoli.BroccoliConfiguration 5 | import de.frosner.broccoli.instances.storage.InstanceStorage 6 | import net.codingwell.scalaguice.ScalaModule 7 | import play.api.inject.ApplicationLifecycle 8 | 9 | import scala.concurrent.Future 10 | 11 | /** 12 | * Module to store instances on the file system. 13 | */ 14 | class FileSystemStorageModule extends AbstractModule with ScalaModule { 15 | override def configure(): Unit = {} 16 | 17 | /** 18 | * Provide the file system instance storage. 19 | * 20 | * @param config Broccoli's configuration 21 | * @param applicationLifecycle The application lifecycle to shutdown the storage 22 | * @return A filesystem storage for instances 23 | */ 24 | @Provides 25 | @Singleton 26 | def provideFileSystemInstanceStorage( 27 | config: BroccoliConfiguration, 28 | applicationLifecycle: ApplicationLifecycle 29 | ): InstanceStorage = { 30 | val storage = new FileSystemInstanceStorage(config.instances.storage.fs.path.toAbsolutePath.toFile) 31 | applicationLifecycle.addStopHook(() => { 32 | if (!storage.isClosed) { 33 | storage.close() 34 | } 35 | Future.successful({}) 36 | }) 37 | storage 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webui/src/Views/LogUrl.elm: -------------------------------------------------------------------------------- 1 | module Views.LogUrl exposing (periodicTaskLog, taskLog) 2 | 3 | import Models.Resources.AllocatedTask exposing (AllocatedTask) 4 | import Models.Resources.LogKind exposing (LogKind(..)) 5 | 6 | 7 | {-| Get the URL to a task log of an instance 8 | -} 9 | taskLog : String -> AllocatedTask -> LogKind -> String 10 | taskLog instanceId task kind = 11 | taskLogHelper instanceId Nothing task kind 12 | 13 | 14 | {-| Get the URL to a periodic task log of an instance 15 | -} 16 | periodicTaskLog : String -> String -> AllocatedTask -> LogKind -> String 17 | periodicTaskLog instanceId periodicJobId task kind = 18 | taskLogHelper instanceId (Just periodicJobId) task kind 19 | 20 | 21 | taskLogHelper : String -> Maybe String -> AllocatedTask -> LogKind -> String 22 | taskLogHelper instanceId maybePeriodicJobId task kind = 23 | String.concat 24 | [ "/downloads/instances/" 25 | , instanceId 26 | , maybePeriodicJobId 27 | |> Maybe.map (\i -> String.concat [ "/periodic/", i ]) 28 | |> Maybe.withDefault "" 29 | , "/allocations/" 30 | , task.allocationId 31 | , "/tasks/" 32 | , task.taskName 33 | , "/logs/" 34 | , case kind of 35 | StdOut -> 36 | "stdout" 37 | 38 | StdErr -> 39 | "stderr" 40 | 41 | -- Only fetch the last 500 KiB of the log, to avoid large requests and download times 42 | , "?offset=500KiB" 43 | ] 44 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/templates/TemplateModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import javax.inject.Singleton 4 | import com.google.inject.{AbstractModule, Provides} 5 | import de.frosner.broccoli.BroccoliConfiguration 6 | import de.frosner.broccoli.signal.UnixSignalManager 7 | import net.codingwell.scalaguice.ScalaModule 8 | 9 | /** 10 | * Provide a template source from the Play configuration 11 | */ 12 | class TemplateModule extends AbstractModule with ScalaModule { 13 | override def configure(): Unit = {} 14 | 15 | /** 16 | * Provide the template source. 17 | * 18 | * @param config The Play configuration 19 | * @return The template source configured from the Play configuration 20 | */ 21 | @Provides 22 | @Singleton 23 | def provideTemplateSource(config: BroccoliConfiguration, 24 | signalManager: UnixSignalManager, 25 | templateRenderer: TemplateRenderer): TemplateSource = 26 | new SignalRefreshedTemplateSource( 27 | new CachedTemplateSource(new DirectoryTemplateSource(config.templates.path, templateRenderer)), 28 | signalManager 29 | ) 30 | 31 | /** 32 | * Provide Template configuration. 33 | * 34 | * @param config The whole broccoli configuration 35 | * @return The templates part of that configuration 36 | */ 37 | @Provides 38 | def provideTemplateConfiguration(config: BroccoliConfiguration): TemplateConfiguration = config.templates 39 | } 40 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/LoggingSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli 2 | 3 | import org.mockito.{ArgumentCaptor, Matchers} 4 | import org.scalacheck.Gen 5 | import org.specs2.ScalaCheck 6 | import org.specs2.mock.Mockito 7 | import org.specs2.mutable.Specification 8 | 9 | import scala.util.matching.Regex 10 | 11 | class LoggingSpec extends Specification with Mockito with ScalaCheck { 12 | import logging._ 13 | 14 | trait F[T] { 15 | def body(): T 16 | 17 | def log(message: String): Unit 18 | } 19 | 20 | "logging the execution time" should { 21 | 22 | "execute the block just once" in { 23 | val f = mock[F[Unit]] 24 | 25 | logExecutionTime("foo") { 26 | f.body() 27 | }(Function.const(())) 28 | 29 | there was one(f).body() 30 | there was no(f).log(Matchers.any[String]()) 31 | } 32 | 33 | "invokes the log function" in prop { label: String => 34 | val f = mock[F[Int]] 35 | 36 | logExecutionTime(label) { 37 | 42 38 | }(f.log(_)) 39 | 40 | val message = ArgumentCaptor.forClass(classOf[String]) 41 | there was one(f).log(message.capture()) 42 | message.getValue must beMatching(s"${Regex.quote(label)} took \\d+ ms") 43 | 44 | there was no(f).body() 45 | }.setGen(Gen.identifier.label("label")) 46 | 47 | "returns the result of the body" in prop { ret: Int => 48 | logExecutionTime("foo") { 49 | ret 50 | }(Function.const(())) === ret 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/templates/SignalRefreshedTemplateSourceSpec.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.templates 2 | 3 | import de.frosner.broccoli.signal.SignalManager 4 | import org.mockito.Mockito.{times, verify} 5 | import org.mockito.{ArgumentCaptor, Matchers} 6 | import org.specs2.mock.Mockito 7 | import org.specs2.mutable.Specification 8 | import sun.misc.{Signal, SignalHandler} 9 | 10 | class SignalRefreshedTemplateSourceSpec extends Specification with Mockito { 11 | "Receiving a SIGUSR2 signal" should { 12 | "update the cache" in { 13 | val signalManager = mock[SignalManager] 14 | val testTemplateSource = mock[CachedTemplateSource] 15 | val signalRefreshedTemplateSource = new SignalRefreshedTemplateSource(testTemplateSource, signalManager) 16 | val handler = ArgumentCaptor.forClass(classOf[SignalHandler]) 17 | there was one(signalManager).register(Matchers.eq(new Signal("USR2")), handler.capture()) 18 | 19 | there was no(testTemplateSource).refresh() 20 | there was no(testTemplateSource).loadTemplates() 21 | signalRefreshedTemplateSource.loadTemplates() 22 | 23 | there was no(testTemplateSource).refresh() 24 | there was one(testTemplateSource).loadTemplates(false) 25 | verify(testTemplateSource, times(1)).loadTemplates(false) 26 | 27 | handler.getValue.handle(new Signal("USR2")) 28 | there was one(testTemplateSource).refresh() 29 | there was one(testTemplateSource).loadTemplates(false) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/test/scala/de/frosner/broccoli/util/TemporaryDirectoryContext.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.util 2 | 3 | import java.io.IOException 4 | import java.nio.file.attribute.BasicFileAttributes 5 | import java.nio.file.{FileVisitResult, FileVisitor, Files, Path} 6 | 7 | import org.specs2.execute.{AsResult, Result} 8 | import org.specs2.specification.ForEach 9 | 10 | /** 11 | * Provide a temporary directory to each test, automatically cleaning up after the test. 12 | */ 13 | trait TemporaryDirectoryContext extends ForEach[Path] { 14 | override protected def foreach[R: AsResult](f: (Path) => R): Result = { 15 | val tempDirectory = Files.createTempDirectory(getClass.getName) 16 | try { 17 | AsResult(f(tempDirectory)) 18 | } finally { 19 | Files.walkFileTree( 20 | tempDirectory, 21 | new FileVisitor[Path] { 22 | override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { 23 | Files.delete(dir) 24 | FileVisitResult.CONTINUE 25 | } 26 | 27 | override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { 28 | Files.delete(file) 29 | FileVisitResult.CONTINUE 30 | } 31 | 32 | override def visitFileFailed(file: Path, exc: IOException): FileVisitResult = throw exc 33 | 34 | override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = 35 | FileVisitResult.CONTINUE 36 | } 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker/test/nomad-server-config/ssl/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID9zCCAt+gAwIBAgIUTuwywdwGs0sgmOvyjfJsBN4Of7gwDQYJKoZIhvcNAQEL 3 | BQAwIzEhMB8GA1UEAxMYaW50ZWdyYXRpb24tdGVzdC1yb290LWNhMB4XDTE5MDIx 4 | MTE1NDg1MFoXDTI5MDExNDE1NDkyMFowJzElMCMGA1UEAxMcbm9tYWQtc2VydmVy 5 | LmludGVncmF0aW9udGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 6 | ALtpn8OO6WpceSIJoe7fvrRzFHKI24oPjmDNvBUlEDogBD7IQltoVjZtwRt13/Ym 7 | QuEZAWU9DjfW1G0E7+nz+8sGNS5gBS1V9t0guIlKbQgbLIX4NLywdYzp2NwbZ0az 8 | 0cbWbDpB6UaFISKHUdReatCRR2jFRW4QFIJVrYds0Mw4DqSUaPn/zvYpgq1Zn0sf 9 | wOMG4mBX5dYkccCWP+Pmdytf4VAJPJ4WXYYA+2um/a5kfCkqsu/Nq2FdG22pLAtD 10 | wg3UZvZfES/0B9Hr9wUMPGFfe132HGTU5nsjCwGu+6gvK4aLALJUyl+byS9AFEw5 11 | suzS3eHRRFKK8R8OKRh/B4UCAwEAAaOCAR0wggEZMA4GA1UdDwEB/wQEAwIDqDAd 12 | BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFCVE95cKmRtF 13 | aPtjLnZrrtWDRlJ5MB8GA1UdIwQYMBaAFFwPLVEKvNxxmR9qVmpe7ScozGpTMDsG 14 | CCsGAQUFBwEBBC8wLTArBggrBgEFBQcwAoYfaHR0cDovLzEyNy4wLjAuMTo4MjAw 15 | L3YxL3BraS9jYTA4BgNVHREEMTAvghxub21hZC1zZXJ2ZXIuaW50ZWdyYXRpb250 16 | ZXN0gglsb2NhbGhvc3SHBH8AAAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEy 17 | Ny4wLjAuMTo4MjAwL3YxL3BraS9jcmwwDQYJKoZIhvcNAQELBQADggEBAA1ZtPm4 18 | mCfci6zsJoXiTBUcFSBPVC6Kzo69XeDx5IiAC5xvMIYhSc3OUnz0paU6wX7s3W1Z 19 | FLez8dKdqZHYXbqL74uxsLpte79+JW+cGrH9Vow1rEVvQv/P9Q0Rh+bl0TbNMZ69 20 | ZLLHCANFUCzGlZa7WS507eJGzmqChoG60JjJAPsQhLrFk0sqP7DGuurLN462mPx9 21 | Ne4xK5Viswtaw2P2NQO+1QirPJ+zZFPJF6rlsFWgTbCCk+KesCwxCo5sxKUFbrYg 22 | +se4V/DI7ixvumPXVr8CraGehxifSnmZVlmLY7nLWrxxdxYzsAlhu5sEl+x+pdJo 23 | o0SHh9x5HOngx08= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/nomad/NomadModule.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.nomad 2 | 3 | import javax.inject.Singleton 4 | 5 | import com.google.inject.{AbstractModule, Provides} 6 | import io.lemonlabs.uri.Url 7 | import de.frosner.broccoli.BroccoliConfiguration 8 | import net.codingwell.scalaguice.ScalaModule 9 | import play.api.libs.ws.WSClient 10 | 11 | import scala.concurrent.ExecutionContext 12 | 13 | /** 14 | * Provide bindings for Nomad access. 15 | */ 16 | class NomadModule extends AbstractModule with ScalaModule { 17 | override def configure(): Unit = {} 18 | 19 | /** 20 | * Provide Nomad configuration. 21 | * 22 | * @param config The whole broccoli configuration 23 | * @return The nomad part of that configuration 24 | */ 25 | @Provides 26 | def provideNomadConfiguration(config: BroccoliConfiguration): NomadConfiguration = config.nomad 27 | 28 | /** 29 | * Provide a nomad client. 30 | * 31 | * The nomad client provided by this method uses Play's client so we let it run on Play's default execution context. 32 | * Play's web service client does not block. 33 | * 34 | * @param config The nomad configuration 35 | * @param wsClient The play web service client to use 36 | * @return A HTTP client for Nomad 37 | */ 38 | @Provides 39 | @Singleton 40 | def provideNomadClient(config: NomadConfiguration, wsClient: WSClient, context: ExecutionContext): NomadClient = 41 | new NomadHttpClient(Url.parse(config.url), config.tokenEnvName, wsClient)(context) 42 | } 43 | -------------------------------------------------------------------------------- /server/src/main/scala/de/frosner/broccoli/auth/AuthConfiguration.scala: -------------------------------------------------------------------------------- 1 | package de.frosner.broccoli.auth 2 | 3 | import scala.concurrent.duration.Duration 4 | 5 | /** 6 | * Authentication configuration. 7 | * 8 | * @param mode The authentication mode 9 | * @param session Configuration for authenticated sessions 10 | * @param cookie Configuration for authentication cookies 11 | * @param conf Configuration for conf authentication mode. 12 | * @param allowedFailedLogins How many failed logins are allowed 13 | */ 14 | final case class AuthConfiguration( 15 | mode: AuthMode, 16 | session: AuthConfiguration.Session, 17 | cookie: AuthConfiguration.Cookie, 18 | conf: AuthConfiguration.Conf, 19 | allowedFailedLogins: Int 20 | ) 21 | 22 | object AuthConfiguration { 23 | 24 | /** 25 | * @param accounts The list of known accounts 26 | */ 27 | final case class Conf(accounts: List[ConfAccount]) 28 | 29 | /** 30 | * @param timeout Timeout until automatic logout 31 | * @param allowMultiLogin Whether a user can login multiple times 32 | */ 33 | final case class Session(timeout: Duration, allowMultiLogin: Boolean) 34 | 35 | final case class Cookie(secure: Boolean) 36 | 37 | /** 38 | * A configured user account. 39 | * 40 | * @param username The username 41 | * @param password The password 42 | * @param instanceRegex The instance regex for the account 43 | * @param role The account role 44 | */ 45 | final case class ConfAccount(username: String, password: String, instanceRegex: String, role: Role) 46 | } 47 | --------------------------------------------------------------------------------