├── 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 |
--------------------------------------------------------------------------------