├── .cfignore ├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── app.js ├── ci ├── image │ ├── Dockerfile.osb-checker-kotlin │ └── README.md ├── pipeline.yml ├── scripts │ ├── conformance.sh │ └── unit.sh └── tasks │ ├── conformance.yml │ └── unit.yml ├── example_schemas ├── allOf-with-two-levels-of-nesting.json ├── allOf.json ├── anyOf-with-two-levels-of-nesting.json ├── anyOf.json ├── default-value-and-not-required.json ├── default-value-and-required.json ├── object-with-min-max-title-description.json ├── oneOf-with-two-levels-of-nesting.json ├── oneOf.json ├── optional-object-with-required-field.json ├── required-object-with-no-required-fields.json ├── required-object-with-required-fields.json ├── three-levels-of-nesting.json └── two-levels-of-nesting-with-required-field-in-second-level.json ├── examples ├── cloudfoundry │ └── manifest.yaml └── kubernetes │ ├── overview-broker-app.yaml │ └── overview-broker-service.yaml ├── extensions ├── health.yaml └── info.yaml ├── helm ├── .helmignore ├── Chart.yaml ├── README.md ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── register.yaml │ ├── service.yaml │ └── serviceaccount.yaml └── values.yaml ├── images ├── cloudfoundry.png ├── kubernetes.png ├── openservicebrokerapi-logo.png ├── openservicebrokerapi-text.png └── openservicebrokerapi.png ├── index.js ├── logger.js ├── package-lock.json ├── package.json ├── service_broker.js ├── service_broker_interface.js ├── tests ├── admin_tests.js ├── extensions.js └── service_broker_interface_tests.js ├── uuid-generator.js └── views ├── dashboard.pug ├── githubcorners.html ├── helpers.js └── style.css /.cfignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: y18n 11 | versions: 12 | - 4.0.1 13 | - 4.0.2 14 | - dependency-name: mocha 15 | versions: 16 | - 8.2.1 17 | - 8.3.0 18 | - 8.3.1 19 | - dependency-name: express-validator 20 | versions: 21 | - 6.10.0 22 | - 6.9.2 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /manifest.yaml 3 | credentials.yml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-slim 2 | LABEL maintainer "Matt McNeeney " 3 | 4 | COPY . / 5 | 6 | RUN npm install 7 | 8 | ENV PORT 8080 9 | EXPOSE 8080 10 | 11 | CMD npm start 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 2 | 3 | This project contains software that is Copyright (c) 2017 Matt McNeeney. 4 | 5 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). 6 | You may not use this product except in compliance with the License. 7 | 8 | This product may include a number of subcomponents with separate copyright notices 9 | and license terms. Your use of these subcomponents is subject to the terms and 10 | conditions of the subcomponent's license, as noted in the LICENSE file. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview Broker 2 | 3 | | Job | Status | 4 | | :-: | :----: | 5 | | Unit | ![Unit status](https://hush-house.pivotal.io/api/v1/teams/marketplace/pipelines/best-broker/jobs/absolute-unit/badge) | 6 | | Conformance | ![Conformance status](https://hush-house.pivotal.io/api/v1/teams/marketplace/pipelines/best-broker/jobs/conformance/badge) | 7 | | [Dockerhub](https://hub.docker.com/r/ismteam/overview-broker) | ![Dockerhub status](https://hush-house.pivotal.io/api/v1/teams/marketplace/pipelines/best-broker/jobs/push-to-dockerhub/badge) | 8 | 9 | A simple service broker conforming to the [Open Service Broker API](https://github.com/openservicebrokerapi/servicebroker/) 10 | specification that hosts a dashboard showing information on service instances 11 | and bindings created by any platform the broker is registered with. 12 | 13 | Other fun features this broker provides include: 14 | * Edit the broker catalog without redeploys to speed up testing 15 | * History of recent requests and responses 16 | * Ability to enable different error modes to test platform integrations 17 | * Change the response mode on the fly (sync only/async only/async where possible) 18 | * A range of configuration parameter schemas for provision service instance, 19 | update service instance and create service binding 20 | * Asynchronous service instance provisions, updates and deletes 21 | * Asynchronous service binding creates and deletes 22 | * Fetching service instances and bindings 23 | * Generic extensions for fetching the [Health](extensions/health.yaml) and 24 | [Info](extensions/info.yaml) for a service instance 25 | 26 | ### What is the Open Service Broker API? 27 | 28 | ![Open Service Broker API](images/openservicebrokerapi.png) 29 | 30 | The [Open Service Broker API](https://www.openservicebrokerapi.org) project 31 | allows developers, ISVs, and SaaS vendors a single, simple, and elegant way to 32 | deliver services to applications running within cloud native platforms such as 33 | Cloud Foundry, OpenShift, and Kubernetes. The project includes individuals from 34 | Fujitsu, Google, IBM, Pivotal, RedHat and SAP. 35 | 36 | ### Quick start 37 | 38 | #### Dockerhub 39 | 40 | The latest version of `overview-broker` can always be found on 41 | [Dockerhub](https://hub.docker.com/r/ismteam/overview-broker). You can 42 | pull and run the latest image with: 43 | ```bash 44 | docker pull ismteam/overview-broker 45 | docker run ismteam/overview-broker 46 | ``` 47 | 48 | #### Build it 49 | ```bash 50 | git clone git@github.com:cloudfoundry/overview-broker.git 51 | cd overview-broker 52 | npm install 53 | 54 | # Start overview-broker 55 | npm start 56 | 57 | # Or to run the tests 58 | npm test 59 | ``` 60 | 61 | #### Configuration 62 | * To set the BasicAuth credentials, set the `BROKER_USERNAME` and 63 | `BROKER_PASSWORD` environmental variables. Otherwise the defaults of `admin` 64 | and `password` will be used. 65 | * To expose a route service, set the `ROUTE_URL` 66 | environmental variable to a url. It must have https scheme. 67 | * To expose a syslog drain service, set the `SYSLOG_DRAIN_URL` 68 | environmental variable to a url. 69 | * To expose a volume mount service, set the `EXPOSE_VOLUME_MOUNT_SERVICE` 70 | environmental variable to `true`. 71 | * To generate many plans with a range of configuration parameter schemas, set 72 | the `ENABLE_EXAMPLE_SCHEMAS` environmental variable to `true`. 73 | * By default, all asynchronous operations take 1 second to complete. To override 74 | this, set the `ASYNCHRONOUS_DELAY_IN_SECONDS` environmental variable to the 75 | number of seconds all operations should take. 76 | * To specify how long platforms should wait before timing out an asynchronous 77 | operation, set the `MAXIMUM_POLLING_DURATION_IN_SECONDS` environmental 78 | variable. 79 | * To specify how long Platforms should wait in between polling the 80 | `/last_operation` endpoint for service instances or bindings, set the 81 | `POLLING_INTERVAL_IN_SECONDS` environmental variable to the number of seconds 82 | a platform should wait before trying again. 83 | * To change the name of the service(s) exposed by the service broker, set the 84 | `SERVICE_NAME` environmental variable. 85 | * To change the description of the service(s) exposed by the service broker, 86 | set the `SERVICE_DESCRIPTION` environmental variable. 87 | * To set the response mode of the service broker (note that this can also be 88 | changed via the broker dashboard), set the `RESPONSE_MODE` environmental 89 | variable to one of the [available modes](app.js#L42). 90 | * To set the error mode of the service broker (note that this can also be 91 | changed via the broker dashboard), set the `ERROR_MODE` environmental 92 | variable to one of the [available modes](app.js#L28). 93 | 94 | --- 95 | 96 | ### Platforms 97 | 98 | #### Cloud Foundry 99 | 100 | ##### 1. Deploying the broker 101 | 102 | * First you will need to deploy the broker as an application: 103 | ```bash 104 | cf push overview-broker -i 1 -m 256M -k 256M --random-route -b https://github.com/cloudfoundry/nodejs-buildpack 105 | ``` 106 | * You can also use an application manifest to deploy the broker as an 107 | application: 108 | ```bash 109 | wget https://raw.githubusercontent.com/cloudfoundry/overview-broker/master/examples/cloudfoundry/manifest.yaml 110 | cf push 111 | ``` 112 | * The overview broker dashboard should now be accessible: 113 | ```bash 114 | open "https://$(cf app the-best-broker | awk '/routes:/{ print $2 }')/dashboard" 115 | ``` 116 | 117 | ##### 2. Registering the broker 118 | 119 | * To register the broker to a space (does not require admin credentials), run: 120 | ```bash 121 | cf create-service-broker --space-scoped overview-broker admin password 122 | ``` 123 | The basic auth credentials "admin" and "password" can be specified if needed 124 | (see [Configuration](#configuration)). 125 | * The services and plans provided by this broker should now be available in the 126 | marketplace: 127 | ```bash 128 | cf marketplace 129 | ``` 130 | 131 | 132 | ##### 3. Creating a service instance 133 | 134 | * Now for the exciting part... it's time to create a new service instance: 135 | ```bash 136 | cf create-service overview-service small my-instance 137 | ``` 138 | You can give your service a specific name in the dashboard by providing the 139 | `name` configuration parameter: 140 | ```bash 141 | cf create-service overview-service small my-instance -c '{ "name": "My Service Instance" }' 142 | ``` 143 | * If you now head back to the dashboard, you should see your new service 144 | instance! 145 | 146 | ##### 4. Creating a service binding 147 | 148 | * To bind the service instance to your application, you will need to first push 149 | an application to Cloud Foundry with `cf push`. You can then create a new 150 | binding with: 151 | ```bash 152 | cf bind-service my-instance 153 | ``` 154 | 155 | #### Kubernetes 156 | 157 | ##### 1. Deploying the broker 158 | 159 | * Deploy the broker and a load balancer that will be used to access it: 160 | ```bash 161 | wget https://raw.githubusercontent.com/cloudfoundry/overview-broker/master/examples/kubernetes/overview-broker-app.yaml 162 | wget https://raw.githubusercontent.com/cloudfoundry/overview-broker/master/examples/kubernetes/overview-broker-service.yaml 163 | kubectl create -f overview-broker-app.yaml 164 | kubectl create -f overview-broker-service.yaml 165 | ``` 166 | You can check this has succeeded by running `kubectl get deployments` and 167 | `kubectl get services`. 168 | * Once the load balancer is up and running, he overview broker dashboard should 169 | be accessible: 170 | ```bash 171 | open "http://$(kubectl get service overview-broker-service -o json | jq -r .status.loadBalancer.ingress[0].ip)/dashboard" 172 | ``` 173 | 174 | ##### 2. Registering the broker 175 | 176 | * To register the broker, you first need to install the Service Catalog. The 177 | instructions to do this can be found 178 | [here](https://github.com/kubernetes-sigs/service-catalog/blob/master/docs/install.md). 179 | If service catalog fails to install due to permissions, you might want to look 180 | at [this guide](https://helm.sh/docs/using_helm/#tiller-and-role-based-access-control). 181 | * You should now be able to register the service broker you deployed earlier 182 | by creating a `clusterservicebrokers` custom resource: 183 | ```bash 184 | BROKER_URL="http://$(kubectl get service overview-broker-service -o json | jq -r .status.loadBalancer.ingress[0].ip)" 185 | cat <" 3 | 4 | RUN apk update && apk add nodejs npm yarn 5 | -------------------------------------------------------------------------------- /ci/image/README.md: -------------------------------------------------------------------------------- 1 | # Image 2 | 3 | The `conformance.yml` task uses the `ismteam/osb-checker-kotlin` Docker image. 4 | The `Dockerfile.osb-checker-kotlin` file is what is used to build that image. 5 | 6 | To rebuild the image if required (e.g. after changing the Dockerfile): 7 | 8 | ``` 9 | docker build -f Dockerfile.osb-checker-kotlin -t ismteam/osb-checker-kotlin . && 10 | docker push ismteam/osb-checker-kotlin 11 | ``` 12 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | - name: absolute-unit 4 | plan: 5 | - get: overview-broker 6 | trigger: true 7 | - task: unit 8 | file: overview-broker/ci/tasks/unit.yml 9 | 10 | - name: conformance 11 | plan: 12 | - get: overview-broker 13 | passed: [absolute-unit] 14 | trigger: true 15 | - get: osb-checker-kotlin 16 | - task: conformance 17 | file: overview-broker/ci/tasks/conformance.yml 18 | 19 | - name: push-to-dockerhub 20 | plan: 21 | - get: overview-broker 22 | passed: [conformance] 23 | trigger: true 24 | - put: dockerhub 25 | params: 26 | build: overview-broker 27 | 28 | resources: 29 | - name: overview-broker 30 | type: git 31 | source: 32 | uri: https://github.com/cloudfoundry/overview-broker 33 | branch: master 34 | 35 | - name: osb-checker-kotlin 36 | type: git 37 | source: 38 | uri: https://github.com/evoila/osb-checker-kotlin.git 39 | branch: master 40 | 41 | - name: dockerhub 42 | type: docker-image 43 | source: 44 | repository: ismteam/overview-broker 45 | username: ((DOCKERHUB_USERNAME)) 46 | password: ((DOCKERHUB_PASSWORD)) 47 | -------------------------------------------------------------------------------- /ci/scripts/conformance.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd overview-broker 4 | npm install 5 | npm start & 6 | 7 | cd ../osb-checker-kotlin 8 | ./gradlew build 9 | 10 | echo """ 11 | config: 12 | url: http://localhost 13 | port: 3000 14 | apiVersion: 2.14 15 | user: admin 16 | password: password 17 | 18 | provisionParameters: 19 | d2d814079edfd33f74b3b454fb666625: 20 | name: instance-name 21 | 22 | bindingParameters: 23 | d2d814079edfd33f74b3b454fb666625: 24 | name: instance-name 25 | """ > application.yml 26 | 27 | java -jar build/libs/*.jar -cat -provision -bind -auth -con 28 | -------------------------------------------------------------------------------- /ci/scripts/unit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install && 4 | npm test 5 | -------------------------------------------------------------------------------- /ci/tasks/conformance.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: ismteam/osb-checker-kotlin 8 | tag: 'latest' 9 | 10 | inputs: 11 | - name: osb-checker-kotlin 12 | - name: overview-broker 13 | 14 | run: 15 | path: overview-broker/ci/scripts/conformance.sh 16 | -------------------------------------------------------------------------------- /ci/tasks/unit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: node 8 | tag: '8' 9 | 10 | inputs: 11 | - name: overview-broker 12 | 13 | run: 14 | dir: overview-broker 15 | path: ci/scripts/unit.sh 16 | -------------------------------------------------------------------------------- /example_schemas/allOf-with-two-levels-of-nesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "baz": { 6 | "type": "object", 7 | "properties": { 8 | "foo": { 9 | "type": "string" 10 | }, 11 | "bar": { 12 | "type": "string" 13 | } 14 | }, 15 | "allOf": [ 16 | { 17 | "required": [ 18 | "foo" 19 | ] 20 | }, 21 | { 22 | "required": [ 23 | "bar" 24 | ] 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example_schemas/allOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "string" 7 | }, 8 | "bar": { 9 | "type": "string" 10 | } 11 | }, 12 | "allOf": [ 13 | { 14 | "required": [ 15 | "foo" 16 | ] 17 | }, 18 | { 19 | "required": [ 20 | "bar" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /example_schemas/anyOf-with-two-levels-of-nesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "baz": { 6 | "type": "object", 7 | "properties": { 8 | "foo": { 9 | "type": "string" 10 | }, 11 | "bar": { 12 | "type": "string" 13 | } 14 | }, 15 | "anyOf": [ 16 | { 17 | "required": [ 18 | "foo" 19 | ] 20 | }, 21 | { 22 | "required": [ 23 | "bar" 24 | ] 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example_schemas/anyOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "string" 7 | }, 8 | "bar": { 9 | "type": "string" 10 | } 11 | }, 12 | "anyOf": [ 13 | { 14 | "required": [ 15 | "foo" 16 | ] 17 | }, 18 | { 19 | "required": [ 20 | "bar" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /example_schemas/default-value-and-not-required.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "integer", 7 | "default": 1 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example_schemas/default-value-and-required.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "integer", 7 | "default": 1 8 | } 9 | }, 10 | "required": [ 11 | "foo" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /example_schemas/object-with-min-max-title-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "title": "This is my title", 5 | "description": "This is my description", 6 | "properties": { 7 | "foo": { 8 | "type": "integer", 9 | "minimum": 0, 10 | "maximum": 5, 11 | "title": "foop", 12 | "description": "this is one hot property" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example_schemas/oneOf-with-two-levels-of-nesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "baz": { 6 | "type": "object", 7 | "properties": { 8 | "foo": { 9 | "type": "string" 10 | }, 11 | "bar": { 12 | "type": "string" 13 | } 14 | }, 15 | "oneOf": [ 16 | { 17 | "required": [ 18 | "foo" 19 | ] 20 | }, 21 | { 22 | "required": [ 23 | "bar" 24 | ] 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example_schemas/oneOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "string" 7 | }, 8 | "bar": { 9 | "type": "string" 10 | } 11 | }, 12 | "oneOf": [ 13 | { 14 | "required": [ 15 | "foo" 16 | ] 17 | }, 18 | { 19 | "required": [ 20 | "bar" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /example_schemas/optional-object-with-required-field.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "object", 7 | "properties": { 8 | "bar": { 9 | "type": "string" 10 | }, 11 | "rainbow": { 12 | "type": "boolean" 13 | } 14 | }, 15 | "required": [ 16 | "bar" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example_schemas/required-object-with-no-required-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "object", 7 | "properties": { 8 | "bar": { 9 | "type": "string" 10 | } 11 | } 12 | } 13 | }, 14 | "required": [ 15 | "foo" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /example_schemas/required-object-with-required-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "object", 7 | "properties": { 8 | "bar": { 9 | "type": "string" 10 | } 11 | }, 12 | "required": [ 13 | "bar" 14 | ] 15 | } 16 | }, 17 | "required": [ 18 | "foo" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /example_schemas/three-levels-of-nesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "object", 7 | "properties": { 8 | "bar": { 9 | "type": "object", 10 | "properties": { 11 | "bazz": { 12 | "type": "object", 13 | "properties": { 14 | "qux": { 15 | "type": "string" 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example_schemas/two-levels-of-nesting-with-required-field-in-second-level.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "foo": { 6 | "type": "object", 7 | "properties": { 8 | "bar": { 9 | "type": "object", 10 | "properties": { 11 | "bazz": { 12 | "type": "string" 13 | } 14 | }, 15 | "required": [ 16 | "bazz" 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/cloudfoundry/manifest.yaml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: the-best-broker 3 | buildpacks: 4 | - https://github.com/cloudfoundry/nodejs-buildpack 5 | instances: 1 6 | memory: 256M 7 | disk_quota: 256M 8 | random-route: false 9 | -------------------------------------------------------------------------------- /examples/kubernetes/overview-broker-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: overview-broker-app 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: overview-broker-app 10 | template: 11 | metadata: 12 | labels: 13 | app: overview-broker-app 14 | spec: 15 | containers: 16 | - name: overview-broker-app 17 | image: ismteam/overview-broker 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 80 21 | 22 | -------------------------------------------------------------------------------- /examples/kubernetes/overview-broker-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: overview-broker-service 5 | spec: 6 | selector: 7 | app: overview-broker-app 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 8080 12 | type: LoadBalancer 13 | 14 | -------------------------------------------------------------------------------- /extensions/health.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Health 4 | description: Fetch health information about a service instance 5 | version: 0.1.0 6 | paths: 7 | /info: 8 | get: 9 | summary: Returns health information for the service instance 10 | responses: 11 | '200': 12 | description: Health 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | alive: 19 | type: boolean 20 | description: is the service instance healthy 21 | -------------------------------------------------------------------------------- /extensions/info.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Resource Info 4 | description: Fetch resource information about a service instance 5 | version: 0.1.0 6 | paths: 7 | /info: 8 | get: 9 | summary: Returns resource information regarding the service instance 10 | responses: 11 | '200': 12 | description: Resource information 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: overview-broker 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | version: 0.1.0 18 | 19 | # This is the version number of the application being deployed. This version number should be 20 | # incremented each time you make changes to the application. 21 | appVersion: latest 22 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | # Helm chart for Overview Broker 2 | 3 | Helm chart which creates a deployment for the overview and registers it with 4 | service catalog 5 | 6 | ``` 7 | helm install test-broker . 8 | ``` 9 | 10 | | Option | Default | Description 11 | |---------|---------|-------------| 12 | |register |true | set `false` to create broker without also creating service-catalog ClusterServiceBroker to register it in the cluster 13 | |brokerUsername | random 24 char alpha numeric | override default generated username 14 | |brokerPassword | random 24 char alpha numeric | override default generated password 15 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "overview-broker.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "overview-broker.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "overview-broker.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "overview-broker.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | echo "Visit http://127.0.0.1:8080 to use your application" 20 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "overview-broker.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "overview-broker.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "overview-broker.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "overview-broker.labels" -}} 38 | helm.sh/chart: {{ include "overview-broker.chart" . }} 39 | {{ include "overview-broker.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "overview-broker.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "overview-broker.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end -}} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "overview-broker.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create -}} 59 | {{ default (include "overview-broker.fullname" .) .Values.serviceAccount.name }} 60 | {{- else -}} 61 | {{ default "default" .Values.serviceAccount.name }} 62 | {{- end -}} 63 | {{- end -}} 64 | -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: overview-broker-secret 5 | type: Opaque 6 | data: 7 | {{- if .Values.brokerUsername }} 8 | username: {{ .Values.brokerUsername | b64enc | quote }} 9 | {{- else }} 10 | username: {{ randAlphaNum 24 | b64enc | quote }} 11 | {{- end }} 12 | {{- if .Values.brokerPassword }} 13 | password: {{ .Values.brokerPassword | b64enc | quote }} 14 | {{- else }} 15 | password: {{ randAlphaNum 24 | b64enc | quote }} 16 | {{- end }} 17 | 18 | --- 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: {{ include "overview-broker.fullname" . }} 23 | labels: 24 | {{- include "overview-broker.labels" . | nindent 4 }} 25 | spec: 26 | replicas: {{ .Values.replicaCount }} 27 | selector: 28 | matchLabels: 29 | {{- include "overview-broker.selectorLabels" . | nindent 6 }} 30 | template: 31 | metadata: 32 | labels: 33 | {{- include "overview-broker.selectorLabels" . | nindent 8 }} 34 | spec: 35 | {{- with .Values.imagePullSecrets }} 36 | imagePullSecrets: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | serviceAccountName: {{ include "overview-broker.serviceAccountName" . }} 40 | securityContext: 41 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 42 | containers: 43 | - name: {{ .Chart.Name }} 44 | securityContext: 45 | {{- toYaml .Values.securityContext | nindent 12 }} 46 | image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" 47 | imagePullPolicy: {{ .Values.image.pullPolicy }} 48 | ports: 49 | - name: http 50 | containerPort: 8080 51 | protocol: TCP 52 | # livenessProbe: 53 | # httpGet: 54 | # path: / 55 | # port: http 56 | # readinessProbe: 57 | # httpGet: 58 | # path: / 59 | # port: http 60 | env: 61 | - name: BROKER_USERNAME 62 | valueFrom: 63 | secretKeyRef: 64 | name: overview-broker-secret 65 | key: username 66 | - name: BROKER_PASSWORD 67 | valueFrom: 68 | secretKeyRef: 69 | name: overview-broker-secret 70 | key: password 71 | resources: 72 | {{- toYaml .Values.resources | nindent 12 }} 73 | {{- with .Values.nodeSelector }} 74 | nodeSelector: 75 | {{- toYaml . | nindent 8 }} 76 | {{- end }} 77 | {{- with .Values.affinity }} 78 | affinity: 79 | {{- toYaml . | nindent 8 }} 80 | {{- end }} 81 | {{- with .Values.tolerations }} 82 | tolerations: 83 | {{- toYaml . | nindent 8 }} 84 | {{- end }} 85 | -------------------------------------------------------------------------------- /helm/templates/register.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.register -}} 2 | apiVersion: servicecatalog.k8s.io/v1beta1 3 | kind: ClusterServiceBroker 4 | metadata: 5 | name: overview-broker 6 | spec: 7 | url: http://{{ include "overview-broker.fullname" . }}.{{ .Release.Namespace }} 8 | authInfo: 9 | basic: 10 | secretRef: 11 | name: overview-broker-secret 12 | namespace: {{ .Release.Namespace }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "overview-broker.fullname" . }} 5 | labels: 6 | {{- include "overview-broker.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "overview-broker.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "overview-broker.serviceAccountName" . }} 6 | labels: 7 | {{- include "overview-broker.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for overview-broker. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: ismteam/overview-broker 9 | pullPolicy: IfNotPresent 10 | 11 | imagePullSecrets: [] 12 | nameOverride: "" 13 | fullnameOverride: "" 14 | 15 | serviceAccount: 16 | # Specifies whether a service account should be created 17 | create: true 18 | # Annotations to add to the service account 19 | annotations: {} 20 | # The name of the service account to use. 21 | # If not set and create is true, a name is generated using the fullname template 22 | name: 23 | 24 | podSecurityContext: {} 25 | # fsGroup: 2000 26 | 27 | securityContext: {} 28 | # capabilities: 29 | # drop: 30 | # - ALL 31 | # readOnlyRootFilesystem: true 32 | # runAsNonRoot: true 33 | # runAsUser: 1000 34 | register: true 35 | 36 | service: 37 | type: ClusterIP 38 | port: 80 39 | 40 | ingress: 41 | enabled: false 42 | annotations: {} 43 | # kubernetes.io/ingress.class: nginx 44 | # kubernetes.io/tls-acme: "true" 45 | hosts: 46 | - host: chart-example.local 47 | paths: [] 48 | tls: [] 49 | # - secretName: chart-example-tls 50 | # hosts: 51 | # - chart-example.local 52 | 53 | resources: {} 54 | # We usually recommend not to specify default resources and to leave this as a conscious 55 | # choice for the user. This also increases chances charts run on environments with little 56 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 57 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 58 | # limits: 59 | # cpu: 100m 60 | # memory: 128Mi 61 | # requests: 62 | # cpu: 100m 63 | # memory: 128Mi 64 | 65 | nodeSelector: {} 66 | 67 | tolerations: [] 68 | 69 | affinity: {} 70 | -------------------------------------------------------------------------------- /images/cloudfoundry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/overview-broker/3b2c197c9b17b6e5b9138851787436598eb18fdf/images/cloudfoundry.png -------------------------------------------------------------------------------- /images/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/overview-broker/3b2c197c9b17b6e5b9138851787436598eb18fdf/images/kubernetes.png -------------------------------------------------------------------------------- /images/openservicebrokerapi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/overview-broker/3b2c197c9b17b6e5b9138851787436598eb18fdf/images/openservicebrokerapi-logo.png -------------------------------------------------------------------------------- /images/openservicebrokerapi-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/overview-broker/3b2c197c9b17b6e5b9138851787436598eb18fdf/images/openservicebrokerapi-text.png -------------------------------------------------------------------------------- /images/openservicebrokerapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry/overview-broker/3b2c197c9b17b6e5b9138851787436598eb18fdf/images/openservicebrokerapi.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | app.start(function() { 4 | // Ready! 5 | }); 6 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | 3 | debug(message) { 4 | if (process.env.NODE_ENV == 'testing') { 5 | return; 6 | } 7 | console.log(message); 8 | } 9 | 10 | } 11 | 12 | module.exports = Logger; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overview-broker", 3 | "version": "1.0.0", 4 | "description": "A service broker providing an overview of platform connections for debugging.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "NODE_ENV=testing mocha --recursive --timeout 10000 --exit tests" 9 | }, 10 | "author": "Matthew McNeeney ", 11 | "repository": "https://github.com/cloudfoundry/overview-broker", 12 | "license": "SEE LICENSE IN LICENSE", 13 | "dependencies": { 14 | "body-parser": "latest", 15 | "cfenv": "^1.2.4", 16 | "express": "^4.17.1", 17 | "express-basic-auth": "^1.2.0", 18 | "express-validator": "^6.14.3", 19 | "jsonschema": "^1.4.0", 20 | "lodash": "^4.17.21", 21 | "moment": "^2.29.1", 22 | "morgan": "^1.10.0", 23 | "pug": "^3.0.2", 24 | "randomstring": "^1.1.5", 25 | "request": "^2.88.2", 26 | "sha256": "latest", 27 | "uuid": "^8.3.2" 28 | }, 29 | "devDependencies": { 30 | "assert": "latest", 31 | "mocha": "^8.3.2", 32 | "should": "^13.2.3", 33 | "supertest": "^6.1.3" 34 | }, 35 | "engines": { 36 | "node": "22.x.x" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service_broker.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | cfenv = require('cfenv'), 3 | sha256 = require('sha256'), 4 | validate = require ('jsonschema').validate, 5 | GenerateUUID = require ('./uuid-generator'), 6 | Logger = require('./logger'); 7 | 8 | class ServiceBroker { 9 | 10 | constructor() { 11 | this.logger = new Logger(); 12 | let serviceName = process.env.SERVICE_NAME || 'overview-service'; 13 | this.catalog = { 14 | services: [ 15 | { 16 | name: serviceName, 17 | description: process.env.SERVICE_DESCRIPTION || 'Provides an overview of any service instances and bindings that have been created by a platform.', 18 | id: GenerateUUID(), 19 | tags: [ 'overview-broker' ], 20 | bindable: true, 21 | plan_updateable: true, 22 | bindings_retrievable: true, 23 | instances_retrievable: true, 24 | metadata: { shareable: true }, 25 | plans: this.generatePlansForService(serviceName), 26 | } 27 | ] 28 | }; 29 | 30 | // Expose a syslog drain service if requested 31 | if (process.env.SYSLOG_DRAIN_URL) { 32 | this.catalog.services.push({ 33 | name: serviceName + '-syslog-drain', 34 | description: 'Provides an example syslog drain service.', 35 | id: GenerateUUID(), 36 | tags: [ 'overview-broker' ], 37 | requires: [ 'syslog_drain' ], 38 | bindable: true, 39 | bindings_retrievable: true, 40 | instances_retrievable: true, 41 | plan_updateable: true, 42 | plans: this.generatePlansForService(serviceName + '-syslog-drain'), 43 | metadata: { shareable: true } 44 | }); 45 | } 46 | 47 | // Expose a volume mount service if requested 48 | if (process.env.EXPOSE_VOLUME_MOUNT_SERVICE) { 49 | this.catalog.services.push({ 50 | name: serviceName + '-volume-mount', 51 | description: 'Provides an example volume mount service.', 52 | id: GenerateUUID(), 53 | tags: [ 'overview-broker' ], 54 | requires: [ 'volume_mount' ], 55 | bindable: true, 56 | bindings_retrievable: true, 57 | instances_retrievable: true, 58 | plan_updateable: true, 59 | plans: this.generatePlansForService(serviceName + '-volume-mount'), 60 | metadata: { shareable: true } 61 | }); 62 | }; 63 | 64 | // Expose a route service if requested 65 | if (process.env.ROUTE_URL) { 66 | this.catalog.services.push({ 67 | name: serviceName + '-route', 68 | description: 'Provides an example route service.', 69 | id: GenerateUUID(), 70 | tags: [ 'overview-broker' ], 71 | requires: [ 'route_forwarding' ], 72 | bindable: true, 73 | bindings_retrievable: true, 74 | instances_retrievable: true, 75 | plan_updateable: true, 76 | plans: this.generatePlansForService(serviceName + '-route'), 77 | metadata: { shareable: true } 78 | }); 79 | } 80 | this.dashboardUrl = `${cfenv.getAppEnv().url}/dashboard`; 81 | logger.debug(`Service broker created. (${this.catalog.services.length} service${this.catalog.services.length == 1 ? '' : 's'} exposed)`); 82 | } 83 | 84 | getCatalog() { 85 | return this.catalog; 86 | } 87 | 88 | setCatalog(data) { 89 | try { 90 | var catalogData = JSON.parse(data); 91 | this.catalog = catalogData; 92 | return null; 93 | } 94 | catch (e) { 95 | return e.toString(); 96 | } 97 | } 98 | 99 | getDashboardUrl() { 100 | return this.dashboardUrl; 101 | } 102 | 103 | getService(serviceId) { 104 | return this.catalog.services.find(function(service) { 105 | return service.id == serviceId; 106 | }); 107 | } 108 | 109 | getPlanForService(serviceId, planId) { 110 | var service = this.getService(serviceId); 111 | if (!service) { 112 | return null; 113 | } 114 | return service.plans.find(function(plan) { 115 | return plan.id == planId; 116 | }); 117 | } 118 | 119 | // getServiceInstanceExtensionAPIs(serviceId) { 120 | // return [ 121 | // { 122 | // discovery_url: '/logs', 123 | // server_url: `${cfenv.getAppEnv().url}/v2/service_instances/${serviceId}`, 124 | // adheres_to: 'http://broker.sapi.life/logs' 125 | // }, 126 | // { 127 | // discovery_url: '/health', 128 | // server_url: `${cfenv.getAppEnv().url}/v2/service_instances/${serviceId}`, 129 | // adheres_to: 'http://broker.sapi.life/health' 130 | // }, 131 | // { 132 | // discovery_url: '/info', 133 | // server_url: `${cfenv.getAppEnv().url}/v2/service_instances/${serviceId}`, 134 | // adheres_to: 'http://broker.sapi.life/info' 135 | // } 136 | // ] 137 | // }; 138 | 139 | validateParameters(schema, parameters) { 140 | var result = validate(parameters, schema); 141 | if (!result.valid) { 142 | return result.errors.toString(); 143 | } 144 | else { 145 | return null; 146 | } 147 | } 148 | 149 | generatePlansForService(serviceName) { 150 | var plans = []; 151 | 152 | // Add a small plan 153 | plans.push({ 154 | name: 'small', 155 | description: 'A small instance of the service.', 156 | free: true, 157 | maintenance_info: { 158 | version: require('./package.json').version 159 | } 160 | }); 161 | 162 | // Add a large plan with a schema 163 | var largePlanSchema = { 164 | $schema: 'http://json-schema.org/draft-04/schema#', 165 | additionalProperties: false, 166 | type: 'object', 167 | properties: { 168 | rainbow: { 169 | type: 'boolean', 170 | default: false, 171 | description: 'Follow the rainbow' 172 | }, 173 | name: { 174 | type: 'string', 175 | minLength: 1, 176 | maxLength: 30, 177 | default: 'This is a default string', 178 | description: 'The name of the broker' 179 | }, 180 | color: { 181 | type: 'string', 182 | enum: [ 'red', 'amber', 'green' ], 183 | default: 'green', 184 | description: 'Your favourite color' 185 | }, 186 | config: { 187 | type: 'object', 188 | properties: { 189 | url: { 190 | type: 'string' 191 | }, 192 | port: { 193 | type: 'integer' 194 | } 195 | } 196 | } 197 | } 198 | }; 199 | plans.push({ 200 | name: 'large', 201 | description: 'A large instance of the service.', 202 | free: true, 203 | maintenance_info: { 204 | version: require('./package.json').version 205 | }, 206 | schemas: { 207 | service_instance: { 208 | create: { 209 | parameters: largePlanSchema 210 | }, 211 | update: { 212 | parameters: largePlanSchema 213 | } 214 | }, 215 | service_binding: { 216 | create: { 217 | parameters: largePlanSchema 218 | } 219 | } 220 | } 221 | }); 222 | 223 | // Load example schemas if requested and generate a plan for each 224 | if (process.env.ENABLE_EXAMPLE_SCHEMAS) { 225 | var exampleSchemas = fs.readdirSync('example_schemas'); 226 | for (var i = 0; i < exampleSchemas.length; i++) { 227 | var name = exampleSchemas[i].split('.json')[0]; 228 | var schema = require(`./example_schemas/${name}`); 229 | plans.push({ 230 | name: name, 231 | description: name.replace(/-/g, ' '), 232 | free: true, 233 | schemas: { 234 | service_instance: { 235 | create: { 236 | parameters: schema 237 | }, 238 | update: { 239 | parameters: schema 240 | } 241 | }, 242 | service_binding: { 243 | create: { 244 | parameters: schema 245 | } 246 | } 247 | } 248 | }); 249 | } 250 | } 251 | 252 | // Add an id to each plan 253 | plans.forEach(function(plan) { 254 | plan.id = GenerateUUID(); 255 | if (parseInt(process.env.MAXIMUM_POLLING_DURATION_IN_SECONDS)) { 256 | plan.maximum_polling_duration = parseInt(process.env.MAXIMUM_POLLING_DURATION_IN_SECONDS); 257 | } 258 | }); 259 | 260 | // All plans generated 261 | return plans; 262 | } 263 | 264 | } 265 | 266 | module.exports = ServiceBroker; 267 | -------------------------------------------------------------------------------- /service_broker_interface.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | moment = require('moment'), 3 | cfenv = require('cfenv'), 4 | randomstring = require('randomstring'), 5 | Logger = require('./logger'), 6 | GenerateUUID = require ('./uuid-generator'), 7 | ServiceBroker = require('./service_broker'); 8 | 9 | const { header, body, param, query, validationResult } = require('express-validator'); 10 | const { NIL } = require('uuid'); 11 | 12 | class ServiceBrokerInterface { 13 | 14 | constructor() { 15 | this.serviceBroker = new ServiceBroker(); 16 | this.logger = new Logger(); 17 | this.serviceInstances = {}; 18 | this.latestRequests = []; 19 | this.latestResponses = []; 20 | this.instanceOperations = {}; 21 | this.bindingOperations = {}; 22 | this.numRequestsToSave = 5; 23 | this.numResponsesToSave = 5; 24 | this.started = moment().toString(); 25 | 26 | // Check for completed asynchronous operations every 10 seconds 27 | var self = this; 28 | setInterval(() => { self.checkAsyncOperations() }, 10000); 29 | } 30 | 31 | checkRequest() { 32 | return [ 33 | // Check for version header 34 | header('X-Broker-Api-Version', 'Missing broker api version').exists(), 35 | (request, response, next) => { 36 | const errors = validationResult(request); 37 | if (!errors.isEmpty()) { 38 | this.sendJSONResponse(response, 412, { error: JSON.stringify(errors) }); 39 | return; 40 | } 41 | next(); 42 | } 43 | ] 44 | } 45 | 46 | getCatalog(request, response) { 47 | var data = this.serviceBroker.getCatalog(); 48 | this.sendJSONResponse(response, 200, data); 49 | } 50 | 51 | createServiceInstance() { 52 | return [ 53 | param('instance_id', 'Missing instance_id').exists(), 54 | body('service_id', 'Missing service_id').exists(), 55 | body('plan_id', 'Missing plan_id').exists(), 56 | body('organization_guid', 'Missing organization_guid').exists(), 57 | body('space_guid', 'Missing space_guid').exists(), 58 | (request, response) => { 59 | const errors = validationResult(request); 60 | if (!errors.isEmpty()) { 61 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 62 | return; 63 | } 64 | 65 | var serviceInstanceId = request.params.instance_id; 66 | let dashboardUrl = `${this.serviceBroker.getDashboardUrl()}?time=${new Date().toISOString()}`; 67 | let data = { 68 | dashboard_url: dashboardUrl 69 | }; 70 | 71 | // Check if we only support asynchronous operations 72 | if (process.env.responseMode == 'async' && request.query.accepts_incomplete != 'true') { 73 | this.sendJSONResponse(response, 422, { error: 'AsyncRequired' } ); 74 | return; 75 | } 76 | 77 | // Check if the instance already exists 78 | if (serviceInstanceId in this.serviceInstances) { 79 | // Check if different sevice or plan ID 80 | if ( 81 | request.body.service_id != this.serviceInstances[serviceInstanceId].service_id || 82 | request.body.plan_id != this.serviceInstances[serviceInstanceId].plan_id || 83 | request.body.organization_guid != this.serviceInstances[serviceInstanceId].organization_guid || 84 | request.body.space_guid != this.serviceInstances[serviceInstanceId].space_guid) { 85 | this.sendJSONResponse(response, 409, { error: 'Service or plan ID does not match' }); 86 | return; 87 | } 88 | // Check if a provision is already in progress 89 | var operation = this.instanceOperations[serviceInstanceId]; 90 | if (operation && operation.type == 'provision' && operation.state == 'in progress') { 91 | this.sendJSONResponse(response, 202, data); 92 | return; 93 | } 94 | this.sendJSONResponse(response, 200, data); 95 | return; 96 | } 97 | 98 | // Validate serviceId and planId 99 | var service = this.serviceBroker.getService(request.body.service_id); 100 | var plan = this.serviceBroker.getPlanForService(request.body.service_id, request.body.plan_id); 101 | if (!plan) { 102 | this.sendJSONResponse(response, 400, { error: `Could not find service ${request.body.service_id}, plan ${request.body.plan_id}` }); 103 | return; 104 | } 105 | 106 | // Validate any configuration parameters if we have a schema 107 | var schema = null; 108 | try { 109 | schema = plan.schemas.service_instance.create.parameters; 110 | } 111 | catch (e) { 112 | // No schema to validate with 113 | } 114 | if (schema) { 115 | var validationErrors = this.serviceBroker.validateParameters(schema, (request.body.parameters || {})); 116 | if (validationErrors) { 117 | this.sendJSONResponse(response, 400, { error: JSON.stringify(validationErrors) }); 118 | return; 119 | } 120 | } 121 | 122 | // Validate maintenance info we were provided it 123 | if (request.body.maintenance_info) { 124 | if (request.body.maintenance_info.version != plan.maintenance_info.version) { 125 | this.sendJSONResponse(response, 422, { error: 'MaintenanceInfoConflict' } ); 126 | return; 127 | } 128 | } 129 | 130 | // Create the service instance 131 | this.logger.debug(`Creating service instance ${serviceInstanceId} using service ${request.body.service_id} and plan ${request.body.plan_id}`); 132 | 133 | this.serviceInstances[serviceInstanceId] = { 134 | created: moment().toString(), 135 | last_updated: 'never', 136 | api_version: request.header('X-Broker-Api-Version'), 137 | service_id: request.body.service_id, 138 | service_name: service.name, 139 | plan_id: request.body.plan_id, 140 | plan_name: plan.name, 141 | parameters: request.body.parameters || {}, 142 | accepts_incomplete: (request.query.accepts_incomplete == 'true'), 143 | organization_guid: request.body.organization_guid, 144 | space_guid: request.body.space_guid, 145 | context: request.body.context || {}, 146 | bindings: {}, 147 | data: data 148 | }; 149 | 150 | if ((request.query.accepts_incomplete == 'true' && (process.env.responseMode == 'default') || process.env.responseMode == 'async')) { 151 | // Set the end time for the operation to be one second from now 152 | // unless an explicit delay was requested 153 | var endTime = new Date(); 154 | if (parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)) { 155 | endTime.setSeconds(endTime.getSeconds() + parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)); 156 | } 157 | else { 158 | endTime.setSeconds(endTime.getSeconds() + 1); 159 | } 160 | this.instanceOperations[serviceInstanceId] = { 161 | type: 'provision', 162 | state: 'in progress', 163 | endTime: endTime 164 | }; 165 | this.sendJSONResponse(response, 202, data); 166 | return; 167 | } 168 | 169 | // Else return the data synchronously 170 | this.sendJSONResponse(response, 201, data); 171 | } 172 | ] 173 | } 174 | 175 | updateServiceInstance() { 176 | return [ 177 | param('instance_id', 'Missing instance_id').exists(), 178 | body('service_id', 'Missing service_id').exists(), 179 | (request, response, next) => { 180 | const errors = validationResult(request); 181 | if (!errors.isEmpty()) { 182 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 183 | return; 184 | } 185 | 186 | var serviceInstanceId = request.params.instance_id; 187 | 188 | var plan = null; 189 | if (request.body.plan_id) { 190 | plan = this.serviceBroker.getPlanForService(request.body.service_id, request.body.plan_id); 191 | } else { 192 | let service_id = this.serviceInstances[serviceInstanceId].service_id; 193 | let plan_id = this.serviceInstances[serviceInstanceId].plan_id; 194 | plan = this.serviceBroker.getPlanForService(service_id, plan_id); 195 | } 196 | 197 | // Validate serviceId and planId 198 | if (!plan) { 199 | this.sendJSONResponse(response, 400, { error: `Could not find service ${request.body.service_id}, plan ${request.body.planid}` }); 200 | return; 201 | } 202 | 203 | // Check if we only support asynchronous operations 204 | if (process.env.responseMode == 'async' && request.query.accepts_incomplete != 'true') { 205 | this.sendJSONResponse(response, 422, { error: 'AsyncRequired' } ); 206 | return; 207 | } 208 | 209 | // Validate any configuration parameters if we have a schema 210 | var schema = null; 211 | try { 212 | schema = plan.schemas.service_instance.update.parameters; 213 | } 214 | catch (e) { 215 | // No schema to validate with 216 | } 217 | if (schema) { 218 | var validationErrors = this.serviceBroker.validateParameters(schema, (request.body.parameters || {})); 219 | if (validationErrors) { 220 | this.sendJSONResponse(response, 400, { error: JSON.stringify(validationErrors) }); 221 | return; 222 | } 223 | } 224 | 225 | // Validate maintenance info we were provided it 226 | if (request.body.maintenance_info) { 227 | if (request.body.maintenance_info.version != plan.maintenance_info.version) { 228 | this.sendJSONResponse(response, 422, { error: 'MaintenanceInfoConflict' } ); 229 | return; 230 | } 231 | } 232 | 233 | this.logger.debug(`Updating service ${serviceInstanceId}`); 234 | 235 | // Check if an operation is in progress 236 | var operation = this.instanceOperations[serviceInstanceId]; 237 | if (operation && operation.state == 'in progress') { 238 | this.sendJSONResponse(response, 422, { error: 'ConcurrencyError' }); 239 | return; 240 | } 241 | 242 | this.serviceInstances[serviceInstanceId].api_version = request.header('X-Broker-Api-Version'), 243 | this.serviceInstances[serviceInstanceId].service_id = request.body.service_id; 244 | this.serviceInstances[serviceInstanceId].plan_id = plan.id; 245 | this.serviceInstances[serviceInstanceId].plan_name = plan.name; 246 | this.serviceInstances[serviceInstanceId].parameters = request.body.parameters || {}; 247 | this.serviceInstances[serviceInstanceId].context = request.body.context || {}; 248 | this.serviceInstances[serviceInstanceId].last_updated = moment().toString(); 249 | 250 | let dashboardUrl = `${this.serviceBroker.getDashboardUrl()}?time=${new Date().toISOString()}`; 251 | let data = { 252 | dashboard_url: dashboardUrl 253 | }; 254 | 255 | if ((request.query.accepts_incomplete == 'true' && (process.env.responseMode == 'default') || process.env.responseMode == 'async')) { 256 | // Set the end time for the operation to be one second from now 257 | // unless an explicit delay was requested 258 | var endTime = new Date(); 259 | if (parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)) { 260 | endTime.setSeconds(endTime.getSeconds() + parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)); 261 | } 262 | else { 263 | endTime.setSeconds(endTime.getSeconds() + 1); 264 | } 265 | this.instanceOperations[serviceInstanceId] = { 266 | type: 'update', 267 | state: 'in progress', 268 | endTime: endTime 269 | }; 270 | this.sendJSONResponse(response, 202, data); 271 | return; 272 | } 273 | 274 | // Else return the data synchronously 275 | this.sendJSONResponse(response, 200, data); 276 | } 277 | ] 278 | } 279 | 280 | deleteServiceInstance() { 281 | return [ 282 | param('instance_id', 'Missing instance_id').exists(), 283 | query('service_id', 'Missing service_id').exists(), 284 | query('plan_id', 'Missing plan_id').exists(), 285 | (request, response, next) => { 286 | const errors = validationResult(request); 287 | if (!errors.isEmpty()) { 288 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 289 | return; 290 | } 291 | 292 | // Validate serviceId and planId 293 | var plan = this.serviceBroker.getPlanForService(request.query.service_id, request.query.plan_id); 294 | if (!plan) { 295 | // Just throw a warning in case the broker was restarted so the IDs changed 296 | console.warn('Could not find service %s, plan %s', request.query.service_id, request.query.plan_id); 297 | } 298 | 299 | // Check if we only support asynchronous operations 300 | if (process.env.responseMode == 'async' && request.query.accepts_incomplete != 'true') { 301 | this.sendJSONResponse(response, 422, { error: 'AsyncRequired' } ); 302 | return; 303 | } 304 | 305 | var serviceInstanceId = request.params.instance_id; 306 | this.logger.debug(`Deleting service ${serviceInstanceId}`); 307 | 308 | // Check if an operation is in progress 309 | var operation = this.instanceOperations[serviceInstanceId]; 310 | if (operation && operation.state == 'in progress') { 311 | // If a provision is in progress, we can cancel it 312 | if (operation.type == 'provision') { 313 | delete this.instanceOperations[serviceInstanceId]; 314 | } 315 | // Else it must be an update so we should fail 316 | else { 317 | this.sendJSONResponse(response, 422, { error: 'ConcurrencyError' }); 318 | return; 319 | } 320 | } 321 | 322 | // Delete the service instance from memory 323 | if (serviceInstanceId in this.serviceInstances) { 324 | delete this.serviceInstances[serviceInstanceId]; 325 | } else { 326 | this.sendJSONResponse(response, 410, {}); 327 | return; 328 | } 329 | 330 | // Perform asynchronous deprovision 331 | if ((request.query.accepts_incomplete == 'true' && (process.env.responseMode == 'default') || process.env.responseMode == 'async')) { 332 | // Set the end time for the operation to be one second from now 333 | // unless an explicit delay was requested 334 | var endTime = new Date(); 335 | if (parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)) { 336 | endTime.setSeconds(endTime.getSeconds() + parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)); 337 | } 338 | else { 339 | endTime.setSeconds(endTime.getSeconds() + 1); 340 | } 341 | this.instanceOperations[serviceInstanceId] = { 342 | type: 'deprovision', 343 | state: 'in progress', 344 | endTime: endTime 345 | }; 346 | this.sendJSONResponse(response, 202, {}); 347 | return; 348 | } 349 | 350 | // Perform synchronous deprovision 351 | this.sendJSONResponse(response, 200, {}); 352 | } 353 | ] 354 | } 355 | 356 | createServiceBinding() { 357 | return [ 358 | param('instance_id', 'Missing instance_id').exists(), 359 | body('service_id', 'Missing service_id').exists(), 360 | body('plan_id', 'Missing plan_id').exists(), 361 | (request, response, next) => { 362 | const errors = validationResult(request); 363 | if (!errors.isEmpty()) { 364 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 365 | return; 366 | } 367 | 368 | var serviceInstanceId = request.params.instance_id; 369 | var bindingId = request.params.binding_id; 370 | 371 | // Check that the instance already exists 372 | if (!this.serviceInstances[serviceInstanceId]) { 373 | this.sendJSONResponse(response, 404, { error: `Could not find service instance ${serviceInstanceId}` }); 374 | return; 375 | } 376 | 377 | // Check if the binding already exists 378 | if (serviceInstanceId in this.serviceInstances && bindingId in this.serviceInstances[serviceInstanceId].bindings) { 379 | // Check if different sevice or plan ID 380 | if (request.body.service_id != this.serviceInstances[serviceInstanceId].bindings[bindingId].service_id || 381 | request.body.plan_id != this.serviceInstances[serviceInstanceId].bindings[bindingId].plan_id) { 382 | this.sendJSONResponse(response, 409, { error: 'Service or plan ID does not match' }); 383 | return; 384 | } 385 | // Check if a bind is already in progress 386 | var operation = this.bindingOperations[bindingId]; 387 | if (operation && operation.type == 'binding' && operation.state == 'in progress') { 388 | this.sendJSONResponse(response, 202, {operation: operation.id }); 389 | return; 390 | } 391 | this.sendJSONResponse(response, 200, this.serviceInstances[serviceInstanceId].bindings[bindingId].data); 392 | return; 393 | } 394 | 395 | // Validate serviceId and planId 396 | var service = this.serviceBroker.getService(request.body.service_id); 397 | if (!service) { 398 | this.sendJSONResponse(response, 400, { error: `Could not find service ${request.body.service_id}` }); 399 | return; 400 | } 401 | var plan = this.serviceBroker.getPlanForService(request.body.service_id, request.body.plan_id); 402 | if (!plan) { 403 | this.sendJSONResponse(response, 400, { error: `Could not find service/plan ${request.body.service_id}/${request.body.plan_id}`}); 404 | return; 405 | } 406 | 407 | // Check if we only support asynchronous operations 408 | if (process.env.responseMode == 'async' && request.query.accepts_incomplete != 'true') { 409 | this.sendJSONResponse(response, 422, { error: 'AsyncRequired' } ); 410 | return; 411 | } 412 | 413 | // Validate any configuration parameters if we have a schema 414 | var schema = null; 415 | try { 416 | schema = plan.schemas.service_binding.create.parameters; 417 | } 418 | catch (e) { 419 | // No schema to validate with 420 | } 421 | if (schema) { 422 | var validationErrors = this.serviceBroker.validateParameters(schema, (request.body.parameters || {})); 423 | if (validationErrors) { 424 | this.sendJSONResponse(response, 400, { error: JSON.stringify(validationErrors) }); 425 | return; 426 | } 427 | } 428 | 429 | this.logger.debug(`Creating service binding ${bindingId} for service ${serviceInstanceId}`); 430 | 431 | // Generate the binding info depending on the type of binding 432 | var data = {}; 433 | if (!service.requires || service.requires.length == 0) { 434 | data = { 435 | credentials: { 436 | username: 'admin', 437 | password: randomstring.generate(16) 438 | } 439 | }; 440 | } 441 | else if (service.requires && service.requires.indexOf('syslog_drain') > -1) { 442 | data = { 443 | syslog_drain_url: process.env.SYSLOG_DRAIN_URL 444 | }; 445 | } 446 | else if (service.requires && service.requires.indexOf('volume_mount') > -1) { 447 | data = { 448 | volume_mounts: [{ 449 | driver: 'nfs', 450 | container_dir: '/tmp', 451 | mode: 'r', 452 | device_type: 'shared', 453 | device: { 454 | volume_id: '1' 455 | } 456 | }] 457 | }; 458 | } 459 | else if (service.requires && service.requires.indexOf('route_forwarding') > -1) { 460 | data = { 461 | route_service_url: process.env.ROUTE_URL 462 | }; 463 | } 464 | 465 | // Save the binding to memory 466 | this.serviceInstances[serviceInstanceId].bindings[bindingId] = { 467 | api_version: request.header('X-Broker-Api-Version'), 468 | service_id: request.body.service_id, 469 | plan_id: request.body.plan_id, 470 | app_guid: request.body.app_guid, 471 | bind_resource: request.body.bind_resource, 472 | parameters: request.body.parameters, 473 | data: data 474 | }; 475 | 476 | // Perform asynchronous binding 477 | if ((request.query.accepts_incomplete == 'true' && (process.env.responseMode == 'default') || process.env.responseMode == 'async')) { 478 | // Set the end time for the operation to be one second from now 479 | // unless an explicit delay was requested 480 | var endTime = new Date(); 481 | if (parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)) { 482 | endTime.setSeconds(endTime.getSeconds() + parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)); 483 | } 484 | else { 485 | endTime.setSeconds(endTime.getSeconds() + 1); 486 | } 487 | this.bindingOperations[bindingId] = { 488 | type: 'binding', 489 | state: 'in progress', 490 | endTime: endTime, 491 | id: GenerateUUID() 492 | }; 493 | this.sendJSONResponse(response, 202, { operation: this.bindingOperations[bindingId].id } ); 494 | return; 495 | } 496 | 497 | // Perform synchronous binding 498 | this.sendJSONResponse(response, 201, data); 499 | } 500 | ] 501 | } 502 | 503 | deleteServiceBinding() { 504 | return [ 505 | param('instance_id', 'Missing instance_id').exists(), 506 | param('binding_id', 'Missing binding_id').exists(), 507 | query('service_id', 'Missing service_id').exists(), 508 | query('plan_id', 'Missing plan_id').exists(), 509 | (request, response, next) => { 510 | const errors = validationResult(request); 511 | if (!errors.isEmpty()) { 512 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 513 | return; 514 | } 515 | 516 | var serviceInstanceId = request.params.instance_id; 517 | var bindingId = request.params.binding_id; 518 | 519 | // Check if we only support asynchronous operations 520 | if (process.env.responseMode == 'async' && request.query.accepts_incomplete != 'true') { 521 | this.sendJSONResponse(response, 422, { error: 'AsyncRequired' } ); 522 | return; 523 | } 524 | 525 | // Check if an operation is in progress 526 | var operation = this.bindingOperations[bindingId]; 527 | if (operation && operation.state == 'in progress') { 528 | this.sendJSONResponse(response, 422, { error: 'ConcurrencyError' }); 529 | return; 530 | } 531 | 532 | this.logger.debug(`Deleting service binding ${bindingId} for service ${serviceInstanceId}`); 533 | 534 | // Delete the service instance from memory 535 | if (serviceInstanceId in this.serviceInstances && bindingId in this.serviceInstances[serviceInstanceId].bindings) { 536 | delete this.serviceInstances[serviceInstanceId].bindings[bindingId]; 537 | } 538 | else { 539 | this.sendJSONResponse(response, 410, {}); 540 | return; 541 | } 542 | 543 | // Perform asynchronous deprovision 544 | if ((request.query.accepts_incomplete == 'true' && (process.env.responseMode == 'default') || process.env.responseMode == 'async')) { 545 | // Set the end time for the operation to be one second from now 546 | // unless an explicit delay was requested 547 | var endTime = new Date(); 548 | if (parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)) { 549 | endTime.setSeconds(endTime.getSeconds() + parseInt(process.env.ASYNCHRONOUS_DELAY_IN_SECONDS)); 550 | } 551 | else { 552 | endTime.setSeconds(endTime.getSeconds() + 1); 553 | } 554 | this.bindingOperations[bindingId] = { 555 | type: 'unbinding', 556 | state: 'in progress', 557 | endTime: endTime, 558 | id: GenerateUUID() 559 | }; 560 | this.sendJSONResponse(response, 202, { operation: this.bindingOperations[bindingId].id }); 561 | return; 562 | } 563 | 564 | // Perform synchronous deprovision 565 | this.sendJSONResponse(response, 200, {}); 566 | } 567 | ] 568 | } 569 | 570 | getLastServiceInstanceOperation() { 571 | return [ 572 | param('instance_id', 'Missing instance_id').exists(), 573 | (request, response, next) => { 574 | const errors = validationResult(request); 575 | if (!errors.isEmpty()) { 576 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 577 | return; 578 | } 579 | var serviceInstanceId = request.params.instance_id; 580 | var operation = this.instanceOperations[serviceInstanceId]; 581 | this.getLastOperation(operation, serviceInstanceId, request, response); 582 | } 583 | ] 584 | } 585 | 586 | getLastServiceBindingOperation() { 587 | return [ 588 | param('instance_id', 'Missing instance_id').exists(), 589 | param('binding_id', 'Missing binding_id').exists(), 590 | (request, response, next) => { 591 | const errors = validationResult(request); 592 | if (!errors.isEmpty()) { 593 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 594 | return; 595 | } 596 | var bindingId = request.params.binding_id; 597 | var operation = this.bindingOperations[bindingId]; 598 | if (request.query.operation != null && request.query.operation != operation.id){ 599 | this.sendJSONResponse(response, 400, { error: "Operation does not match" }); 600 | return; 601 | } 602 | this.getLastOperation(operation, bindingId, request, response); 603 | } 604 | ] 605 | } 606 | 607 | checkAsyncOperations() { 608 | var self = this; 609 | Object.keys(this.instanceOperations).forEach(function(key) { 610 | self.updateOperation(self.instanceOperations[key], key); 611 | }); 612 | Object.keys(this.bindingOperations).forEach(function(key) { 613 | self.updateOperation(self.bindingOperations[key], key); 614 | }); 615 | } 616 | 617 | updateOperation(operation, id) { 618 | // Exit early if should never finish 619 | if (process.env.errorMode == 'neverfinishasync') { 620 | return 621 | } 622 | 623 | // Check if the operation has finished 624 | if (operation.state == 'in progress' && operation.endTime < new Date()) { 625 | // Check if we should fail the operation 626 | operation.state = process.env.errorMode == 'failasync' ? 'failed' : 'succeeded'; 627 | this.logger.debug(`Operation of type ${operation.type} completed with state ${operation.state} (id: ${id})`); 628 | } 629 | } 630 | 631 | getLastOperation(operation, id, request, response) { 632 | // If we don't know about the operation, presume that it failed since we have forgotten about it 633 | if (!operation) { 634 | this.sendJSONResponse(response, 200, { 635 | state: 'failed', 636 | description: 'The operation could not be found.' 637 | }); 638 | return; 639 | } 640 | 641 | // Update the operation in case it has finished 642 | this.updateOperation(operation, id); 643 | 644 | // Check if the operation is still going 645 | if (operation.state == 'in progress') { 646 | // Check if we should add a Retry-After header 647 | if (parseInt(process.env.POLLING_INTERVAL_IN_SECONDS)) { 648 | response.append('Retry-After', parseInt(process.env.POLLING_INTERVAL_IN_SECONDS)); 649 | } 650 | } 651 | 652 | // If this was a deprovision or delete binding operation that succeeded, return 410 653 | if (operation.state == 'succeeded' && 654 | (operation.type == 'deprovision' || operation.type == 'unbinding')) { 655 | this.sendJSONResponse(response, 410, { 656 | state: operation.state, 657 | description: `Operation ${operation.state}` 658 | }); 659 | return; 660 | } 661 | 662 | // Else return 200 663 | this.sendJSONResponse(response, 200, { 664 | state: operation.state, 665 | description: `Operation ${operation.state}` 666 | }); 667 | } 668 | 669 | getServiceInstance() { 670 | return [ 671 | param('instance_id', 'Missing instance_id').exists(), 672 | (request, response, next) => { 673 | const errors = validationResult(request); 674 | if (!errors.isEmpty()) { 675 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 676 | return; 677 | } 678 | 679 | let serviceInstanceId = request.params.instance_id; 680 | if (!this.serviceInstances[serviceInstanceId]) { 681 | this.sendJSONResponse(response, 404, { error: `Could not find service instance ${serviceInstanceId}` }); 682 | return; 683 | } 684 | 685 | var data = Object.assign({}, this.serviceInstances[serviceInstanceId].data); 686 | data.service_id = this.serviceInstances[serviceInstanceId].service_id; 687 | data.plan_id = this.serviceInstances[serviceInstanceId].plan_id; 688 | data.parameters = this.serviceInstances[serviceInstanceId].parameters; 689 | 690 | this.sendJSONResponse(response, 200, data); 691 | } 692 | ] 693 | } 694 | 695 | getServiceBinding() { 696 | return [ 697 | param('instance_id', 'Missing instance_id').exists(), 698 | param('binding_id', 'Missing binding_id').exists(), 699 | (request, response, next) => { 700 | const errors = validationResult(request); 701 | if (!errors.isEmpty()) { 702 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 703 | return; 704 | } 705 | 706 | let serviceInstanceId = request.params.instance_id; 707 | let bindingId = request.params.binding_id; 708 | if (!this.serviceInstances[serviceInstanceId]) { 709 | this.sendJSONResponse(response, 404, { error: `Could not find service instance ${serviceInstanceId}` }); 710 | return; 711 | } 712 | if (!this.serviceInstances[serviceInstanceId].bindings[bindingId]) { 713 | this.sendJSONResponse(response, 404, { error: `Could not find service binding ${bindingId}` }); 714 | return; 715 | } 716 | 717 | var data = Object.assign({}, this.serviceInstances[serviceInstanceId].bindings[bindingId].data); 718 | data.parameters = this.serviceInstances[serviceInstanceId].bindings[bindingId].parameters; 719 | 720 | this.sendJSONResponse(response, 200, data); 721 | } 722 | ] 723 | } 724 | 725 | getDashboardData() { 726 | return { 727 | title: 'Overview Broker', 728 | started: this.started, 729 | serviceInstances: this.serviceInstances, 730 | latestRequests: this.latestRequests.slice().reverse(), 731 | latestResponses: this.latestResponses.slice().reverse(), 732 | catalog: this.serviceBroker.getCatalog(), 733 | env: { 734 | BROKER_USERNAME: process.env.BROKER_USERNAME || 'admin', 735 | BROKER_PASSWORD: process.env.BROKER_PASSWORD || 'password', 736 | SYSLOG_DRAIN_URL: process.env.SYSLOG_DRAIN_URL, 737 | ROUTE_URL: process.env.ROUTE_URL, 738 | EXPOSE_VOLUME_MOUNT_SERVICE: process.env.EXPOSE_VOLUME_MOUNT_SERVICE, 739 | ENABLE_EXAMPLE_SCHEMAS: process.env.ENABLE_EXAMPLE_SCHEMAS, 740 | ASYNCHRONOUS_DELAY_IN_SECONDS: process.env.ASYNCHRONOUS_DELAY_IN_SECONDS, 741 | MAXIMUM_POLLING_DURATION_IN_SECONDS: process.env.MAXIMUM_POLLING_DURATION_IN_SECONDS, 742 | POLLING_INTERVAL_IN_SECONDS: process.env.POLLING_INTERVAL_IN_SECONDS, 743 | SERVICE_NAME: process.env.SERVICE_NAME, 744 | SERVICE_DESCRIPTION: process.env.SERVICE_DESCRIPTION 745 | } 746 | }; 747 | } 748 | 749 | getHealth() { 750 | return [ 751 | param('instance_id', 'Missing instance_id').exists(), 752 | (request, response, next) => { 753 | const errors = validationResult(request); 754 | if (!errors.isEmpty()) { 755 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 756 | return; 757 | } 758 | 759 | let serviceInstanceId = request.params.instance_id; 760 | if (!this.serviceInstances[serviceInstanceId]) { 761 | this.sendJSONResponse(response, 200, { alive: false }); 762 | return; 763 | } 764 | 765 | this.sendJSONResponse(response, 200, { alive: true }); 766 | } 767 | ] 768 | } 769 | 770 | getInfo() { 771 | return [ 772 | param('instance_id', 'Missing instance_id').exists(), 773 | (request, response, next) => { 774 | const errors = validationResult(request); 775 | if (!errors.isEmpty()) { 776 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 777 | return; 778 | } 779 | 780 | let serviceInstanceId = request.params.instance_id; 781 | if (!this.serviceInstances[serviceInstanceId]) { 782 | this.sendJSONResponse(response, 404, { error: `Could not find service instance ${serviceInstanceId}` }); 783 | return; 784 | } 785 | 786 | let data = { 787 | server_url: cfenv.getAppEnv().url, 788 | npm_config_node_version: process.env.npm_config_node_version, 789 | npm_package_version: process.env.npm_package_version, 790 | }; 791 | this.sendJSONResponse(response, 200, data); 792 | } 793 | ] 794 | } 795 | 796 | getLogs(request, response) { 797 | request.checkParams('instance_id', 'Missing instance_id').notEmpty(); 798 | var errors = request.validationErrors(); 799 | if (errors) { 800 | this.sendJSONResponse(response, 400, { error: JSON.stringify(errors) }); 801 | return; 802 | } 803 | 804 | let serviceInstanceId = request.params.instance_id; 805 | if (!this.serviceInstances[serviceInstanceId]) { 806 | this.sendJSONResponse(response, 404, { error: `Could not find service instance ${serviceInstanceId}` }); 807 | return; 808 | } 809 | 810 | 811 | this.sendJSONResponse(response, 200, data); 812 | } 813 | 814 | listInstances(request, response) { 815 | var data = {}; 816 | var serviceInstances = this.serviceInstances; 817 | Object.keys(serviceInstances).forEach(function(key) { 818 | data[key] = serviceInstances[key].data; 819 | }); 820 | this.sendJSONResponse(response, 200, data); 821 | } 822 | 823 | clean(request, response) { 824 | this.serviceInstances = {}; 825 | this.latestRequests = []; 826 | this.latestResponses = []; 827 | this.instanceOperations = {}; 828 | this.bindingOperations = {}; 829 | response.status(200).json({}); 830 | } 831 | 832 | updateCatalog(request, response) { 833 | let data = request.body.catalog; 834 | let error = this.serviceBroker.setCatalog(data); 835 | if (error) { 836 | this.sendJSONResponse(response, 400, { error: JSON.stringify(error) }); 837 | return; 838 | } 839 | this.sendJSONResponse(response, 200, {}); 840 | } 841 | 842 | saveRequest(request) { 843 | this.latestRequests.push({ 844 | timestamp: moment().toString(), 845 | data: { 846 | url: request.url, 847 | method: request.method, 848 | body: request.body, 849 | headers: request.headers 850 | } 851 | }); 852 | if (this.latestRequests.length > this.numRequestsToSave) { 853 | this.latestRequests.shift(); 854 | } 855 | } 856 | 857 | saveResponse(httpCode, data, headers) { 858 | this.latestResponses.push({ 859 | timestamp: moment().toString(), 860 | data: { 861 | code: httpCode, 862 | headers: headers, 863 | body: data 864 | } 865 | }); 866 | if (this.latestResponses.length > this.numResponsesToSave) { 867 | this.latestResponses.shift(); 868 | } 869 | } 870 | 871 | sendJSONResponse(response, httpCode, data) { 872 | response.status(httpCode).json(data); 873 | this.saveResponse(httpCode, data, response.getHeaders()); 874 | } 875 | 876 | sendResponse(response, httpCode, data) { 877 | response.status(httpCode).send(data); 878 | this.saveResponse(httpCode, data, response.getHeaders()); 879 | } 880 | 881 | getServiceBroker() { 882 | return this.serviceBroker; 883 | } 884 | 885 | } 886 | 887 | module.exports = ServiceBrokerInterface; 888 | -------------------------------------------------------------------------------- /tests/admin_tests.js: -------------------------------------------------------------------------------- 1 | let should = require('should'), 2 | request = require('supertest'), 3 | app = require('./../app'); 4 | 5 | describe('Admin', function() { 6 | 7 | var server = null; 8 | var catalog = null; 9 | 10 | before(function(done) { 11 | app.start(function(s, sbInterface) { 12 | let serviceBroker = sbInterface.getServiceBroker(); 13 | catalog = serviceBroker.getCatalog(); 14 | server = s; 15 | done(); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | server.close(() => { 21 | done(); 22 | }); 23 | }); 24 | 25 | describe('cleaning', function() { 26 | 27 | it('should succeed', function(done) { 28 | request(server) 29 | .post('/admin/clean') 30 | .expect(200) 31 | .then(response => { 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | describe('update catalog', function() { 39 | 40 | it('should succeed', function(done) { 41 | request(server) 42 | .post('/admin/updateCatalog') 43 | .send({ catalog: JSON.stringify(catalog) }) 44 | .expect(200) 45 | .then(response => { 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | 52 | describe('error mode', function() { 53 | 54 | it('should set to disabled', function(done) { 55 | request(server) 56 | .post('/admin/setErrorMode') 57 | .send({ mode: '' }) 58 | .expect(200) 59 | .then(response => { 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should set to timeout', function(done) { 65 | request(server) 66 | .post('/admin/setErrorMode') 67 | .send({ mode: 'timeout' }) 68 | .expect(200) 69 | .then(response => { 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should set to servererror', function(done) { 75 | request(server) 76 | .post('/admin/setErrorMode') 77 | .send({ mode: 'servererror' }) 78 | .expect(200) 79 | .then(response => { 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should set to badrequest', function(done) { 85 | request(server) 86 | .post('/admin/setErrorMode') 87 | .send({ mode: 'badrequest' }) 88 | .expect(200) 89 | .then(response => { 90 | done(); 91 | }); 92 | }); 93 | 94 | it('should set to notfound', function(done) { 95 | request(server) 96 | .post('/admin/setErrorMode') 97 | .send({ mode: 'notfound' }) 98 | .expect(200) 99 | .then(response => { 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should set to gone', function(done) { 105 | request(server) 106 | .post('/admin/setErrorMode') 107 | .send({ mode: 'gone' }) 108 | .expect(200) 109 | .then(response => { 110 | done(); 111 | }); 112 | }); 113 | 114 | it('should set to unprocessable', function(done) { 115 | request(server) 116 | .post('/admin/setErrorMode') 117 | .send({ mode: 'unprocessable' }) 118 | .expect(200) 119 | .then(response => { 120 | done(); 121 | }); 122 | }); 123 | 124 | it('should set to concurrencyerror', function(done) { 125 | request(server) 126 | .post('/admin/setErrorMode') 127 | .send({ mode: 'concurrencyerror' }) 128 | .expect(200) 129 | .then(response => { 130 | done(); 131 | }); 132 | }); 133 | 134 | it('should set to 200invalidjson', function(done) { 135 | request(server) 136 | .post('/admin/setErrorMode') 137 | .send({ mode: '200invalidjson' }) 138 | .expect(200) 139 | .then(response => { 140 | done(); 141 | }); 142 | }); 143 | 144 | it('should set to 201invalidjson', function(done) { 145 | request(server) 146 | .post('/admin/setErrorMode') 147 | .send({ mode: '201invalidjson' }) 148 | .expect(200) 149 | .then(response => { 150 | done(); 151 | }); 152 | }); 153 | 154 | it('should set to invalidsuccesscode', function(done) { 155 | request(server) 156 | .post('/admin/setErrorMode') 157 | .send({ mode: 'invalidsuccesscode' }) 158 | .expect(200) 159 | .then(response => { 160 | done(); 161 | }); 162 | }); 163 | 164 | it('should set to failasync', function(done) { 165 | request(server) 166 | .post('/admin/setErrorMode') 167 | .send({ mode: 'failasync' }) 168 | .expect(200) 169 | .then(response => { 170 | done(); 171 | }); 172 | }); 173 | 174 | it('should set to neverfinishasync', function(done) { 175 | request(server) 176 | .post('/admin/setErrorMode') 177 | .send({ mode: 'neverfinishasync' }) 178 | .expect(200) 179 | .then(response => { 180 | done(); 181 | }); 182 | }); 183 | 184 | }); 185 | 186 | }); 187 | -------------------------------------------------------------------------------- /tests/extensions.js: -------------------------------------------------------------------------------- 1 | let should = require('should'), 2 | request = require('supertest'), 3 | app = require('./../app'); 4 | 5 | describe('Extensions', function() { 6 | 7 | var server = null; 8 | var catalog = null; 9 | 10 | before(function(done) { 11 | app.start(function(s, sbInterface) { 12 | let serviceBroker = sbInterface.getServiceBroker(); 13 | catalog = serviceBroker.getCatalog(); 14 | server = s; 15 | done(); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | server.close(() => { 21 | done(); 22 | }); 23 | }); 24 | 25 | describe('health', function() { 26 | 27 | it('should fetch discovery doc', function(done) { 28 | request(server) 29 | .get('/health') 30 | .expect(200) 31 | .then(response => { 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | describe('info', function() { 39 | 40 | it('should fetch discovery doc', function(done) { 41 | request(server) 42 | .get('/info') 43 | .expect(200) 44 | .then(response => { 45 | done(); 46 | }); 47 | }); 48 | 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /uuid-generator.js: -------------------------------------------------------------------------------- 1 | function GenerateUUID() { 2 | var dt = new Date().getTime(); 3 | 4 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 5 | var r = (dt + Math.random()*16)%16 | 0; 6 | dt = Math.floor(dt/16); 7 | return (c=='x' ? r :(r&0x3|0x8)).toString(16); 8 | }); 9 | 10 | return uuid; 11 | } 12 | 13 | module.exports = GenerateUUID; 14 | -------------------------------------------------------------------------------- /views/dashboard.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | link(rel='icon', href='/images/openservicebrokerapi-logo.png') 5 | link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css') 6 | link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css') 7 | style 8 | include style.css 9 | body 10 | - 11 | function syntaxHighlight(json) { 12 | if (typeof json != 'string') { 13 | json = JSON.stringify(json, undefined, 2); 14 | } 15 | json = json.replace(/&/g, '&').replace(//g, '>'); 16 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 17 | var cls = 'number'; 18 | if (/^"/.test(match)) { 19 | if (/:$/.test(match)) { 20 | cls = 'key'; 21 | } else { 22 | cls = 'string'; 23 | } 24 | } else if (/true|false/.test(match)) { 25 | cls = 'boolean'; 26 | } else if (/null/.test(match)) { 27 | cls = 'null'; 28 | } 29 | return '' + match + ''; 30 | }); 31 | } 32 | .container 33 | .header.clearfix(style={'margin-top': '15px'}) 34 | div.col-12 35 | img.spin(src='../images/openservicebrokerapi-logo.png', alt='Open Service Broker API Logo') 36 | img(src='../images/openservicebrokerapi-text.png', alt='Open Service Broker API') 37 | div.row.mb-2 38 | div.col-12 39 | span.float-right 40 | button.btn.btn-info(style={'margin-right': '5px'}, onclick='help()') What is this? 41 | button.btn.btn-danger(style={'margin-right': '5px'}, onclick='cleanData()') Clean 42 | button.btn.btn-secondary(onclick='refreshPage()') Refresh 43 | div.row.mb-2 44 | div.col-12 45 | div.card 46 | h6.card-header 47 | a.text-primary(data-toggle='collapse', href='#settings') Settings 48 | div#settings.card-body.collapse 49 | div.form-group 50 | label Started: 51 | pre(style={'background': '#eee', 'border-radius': '4px'}) 52 | span #{started} 53 | div.form-group 54 | label Environmental variables: 55 | pre(style={'background': '#eee', 'border-radius': '4px'}) 56 | span !{syntaxHighlight(env)} 57 | div.form-group 58 | label(for='errorModeSelect') Error mode: 59 | select.form-control(id='errorModeSelect', onchange='errorModeChanged(this)') 60 | option(code='', selected=(errorMode == '')) Disabled 61 | option(code='servererror', selected=(errorMode == 'servererror')) Respond with HTTP 500 62 | option(code='badrequest', selected=(errorMode == 'badrequest')) Respond with HTTP 400 63 | option(code='notfound', selected=(errorMode == 'notfound')) Respond with HTTP 404 64 | option(code='gone', selected=(errorMode == 'gone')) Respond with HTTP 410 65 | option(code='unprocessable', selected=(errorMode == 'unprocessable')) Respond with HTTP 422 66 | option(code='concurrencyerror', selected=(errorMode == 'concurrencyerror')) Respond with HTTP 422 with ConcurencyError code 67 | option(code='maintenanceinfoconflict', selected=(errorMode == 'maintenanceinfoconflict')) Respond with HTTP 422 with MaintenanceInfoConflict code 68 | option(code='timeout', selected=(errorMode == 'timeout')) Do not respond (timeout) 69 | option(code='200invalidjson', selected=(errorMode == '200invalidjson')) Respond with 200 OK invalid JSON 70 | option(code='201invalidjson', selected=(errorMode == '201invalidjson')) Respond with 201 OK invalid JSON 71 | option(code='invalidsuccesscode', selected=(errorMode == 'invalidsuccesscode')) Respond with 204 No Contect invalid success 72 | option(code='failasync', selected=(errorMode == 'failasync')) Fail asynchronous operations (after they have started) 73 | option(code='neverfinishasync', selected=(errorMode == 'neverfinishasync')) Never finish asynchronous operations 74 | div.form-group 75 | label(for='responseModeSelect') Response mode: 76 | select.form-control(id='responseModeSelect', onchange='responseModeChanged(this)') 77 | option(code='default', selected=(responseMode == 'default')) Asynchronous responses where possible 78 | option(code='sync', selected=(responseMode == 'sync')) Synchronous responses always 79 | option(code='async', selected=(responseMode == 'async')) Asynchronous responses always 80 | div.form-group 81 | button.btn.btn-primary(onclick="editCatalog(\'" + JSON.stringify(catalog) + "\', true)") Edit the catalog 82 | div.row.mb-2 83 | div.col-12.col-sm-6 84 | div.card 85 | h6.card-header 86 | a.text-info(data-toggle='collapse', href='#request') Recent Requests 87 | div#request.card-body.collapse.show(style={'padding': '0px'}) 88 | pre(style={'background': 'none', 'border': 'none', 'height': '300px'}) 89 | each request, index in latestRequests 90 | p [#{index+1}] #{request.timestamp} 91 | p !{syntaxHighlight(request.data)} 92 | div.col-12.col-sm-6 93 | div.card 94 | h6.card-header 95 | a.text-info(data-toggle='collapse', href='#response') Recent Responses 96 | div#response.card-body.collapse.show(style={'padding': '0px'}) 97 | pre(style={'background': 'none', 'border': 'none', 'height': '300px'}) 98 | each response, index in latestResponses 99 | p [#{index+1}] #{response.timestamp} 100 | p !{syntaxHighlight(response.data)} 101 | - var numServices = Object.keys(serviceInstances).length 102 | if numServices > 0 103 | div.row.mb-2 104 | - var a = 0 105 | each serviceData, serviceKey in serviceInstances 106 | - 107 | var serviceDataToDisplay = JSON.parse(JSON.stringify(serviceData)) 108 | JSON.stringify(serviceDataToDisplay) 109 | delete serviceDataToDisplay['created'] 110 | delete serviceDataToDisplay['last_updated'] 111 | delete serviceDataToDisplay['data'] 112 | div.col-12.col-md-6.mb-2 113 | div.card.instance 114 | div.card-header(class=`${serviceData.parameters.rainbow ? "rainbow" : ""}`) 115 | h6 116 | if serviceDataToDisplay.context.instance_name != null 117 | span !{serviceDataToDisplay.context.instance_name} 118 | else 119 | span Service Instance 120 | if serviceDataToDisplay.context.platform == 'cloudfoundry' && serviceDataToDisplay.context.organization_name != null && serviceDataToDisplay.context.space_name != null 121 | span (org: #{serviceDataToDisplay.context.organization_name}, space: #{serviceDataToDisplay.context.space_name}) 122 | if serviceDataToDisplay.context.platform == 'kubernetes' && serviceDataToDisplay.context.namespace != null 123 | span (namespace: #{serviceDataToDisplay.context.namespace}) 124 | div.card-body 125 | div.float-right 126 | if serviceDataToDisplay.context != null && serviceDataToDisplay.context.platform == 'kubernetes' 127 | img(src='../images/kubernetes.png', alt='Kubernetes', style={'height': '40px', 'width': 'auto', 'margin-top': '-10px'}) 128 | else 129 | img(src='../images/cloudfoundry.png', alt='Cloud Foundry', style={'height': '40px', 'width': 'auto', 'margin-top': '-10px'}) 130 | p.text-muted ID: #{serviceKey} 131 | p.text-muted Created: #{serviceData.created} 132 | if serviceData.last_update 133 | p.text-muted Last Update: #{serviceData.last_update} 134 | pre(style={'background': 'none', 'border': 'none'}) !{syntaxHighlight(serviceDataToDisplay)} 135 | - a+=1 136 | if (a % 2) == 0 137 | div.clearfix 138 | else 139 | div.col-12(style={'margin-top': '50px', 'text-align': 'center'}) 140 | p No service instances have been provisioned 141 | 142 | include githubcorners.html 143 | 144 | script(src='https://code.jquery.com/jquery-3.2.1.min.js') 145 | script(src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js') 146 | script(src='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js') 147 | script(src='https://cdn.jsdelivr.net/npm/sweetalert@2.1.0/dist/sweetalert.min.js') 148 | script 149 | include helpers.js 150 | -------------------------------------------------------------------------------- /views/githubcorners.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function help() { 4 | var content = document.createElement('div'); 5 | content.innerHTML = `

👋 Hey, I'm Overview Broker!



` + 6 | `I am a simple service broker that you can use to test service instance and service binding workflows.

` + 7 | `The easiest way to use me is to register this URL with your platform:
` + `${window.location.origin}

` + 8 | `For example, in Cloud Foundry, you can do this by running the following command (you will need to use different credentials if you deployed me yourself and configured these):

` + 9 | `cf create-service-broker overview-broker admin password ${window.location.origin}`; 10 | swal({ 11 | content: content, 12 | buttons: { 13 | nothanks: { 14 | text: 'No thanks', 15 | className: 'swal-button--cancel' 16 | }, 17 | thanks: 'Thanks!' 18 | } 19 | }).then(function(result) { 20 | if (result == 'nothanks') { 21 | window.open('https://github.com/cloudfoundry/overview-broker/issues/new', '_blank'); 22 | } 23 | }); 24 | } 25 | 26 | function cleanData() { 27 | swal({ 28 | title: 'Are you sure?', 29 | text: 'You will not be able to recover the service instance data.', 30 | icon: 'warning', 31 | buttons: { 32 | cancel: { 33 | text: 'Cancel', 34 | visible: true 35 | }, 36 | confirm: { 37 | text: 'Delete', 38 | visible: true, 39 | closeModal: false 40 | } 41 | } 42 | }).then(function(result) { 43 | if (!result) { 44 | return; 45 | } 46 | jQuery.post('/admin/clean', function() { 47 | swal({ 48 | title: 'Completed', 49 | text: 'Service instance data has been deleted.', 50 | icon: 'success' 51 | }).then(function() { 52 | refreshPage(); 53 | }); 54 | }).fail(function() { 55 | swal({ 56 | title: 'Oops...', 57 | text: 'There was a problem removing service instance data. Please try again.', 58 | icon: 'error' 59 | }); 60 | }); 61 | }); 62 | } 63 | 64 | function errorModeChanged(el) { 65 | let errorMode = el.options[el.selectedIndex].attributes.code.value; 66 | jQuery.post('/admin/setErrorMode', { mode: errorMode }, function() { 67 | swal({ 68 | title: 'Completed', 69 | text: `The error mode has been updated`, 70 | icon: 'success', 71 | buttons: false, 72 | timer: 1000 73 | }); 74 | }).fail(function() { 75 | swal({ 76 | title: 'Oops...', 77 | text: 'There was a problem setting the error mode. Please try again.', 78 | icon: 'error' 79 | }); 80 | }); 81 | } 82 | 83 | function responseModeChanged(el) { 84 | let responseMode = el.options[el.selectedIndex].attributes.code.value; 85 | jQuery.post('/admin/setResponseMode', { mode: responseMode }, function() { 86 | swal({ 87 | title: 'Completed', 88 | text: `The response mode has been updated`, 89 | icon: 'success', 90 | buttons: false, 91 | timer: 1000 92 | }); 93 | }).fail(function() { 94 | swal({ 95 | title: 'Oops...', 96 | text: 'There was a problem setting the response mode. Please try again.', 97 | icon: 'error' 98 | }); 99 | }); 100 | } 101 | 102 | function editCatalog(catalogText, prettify) { 103 | var prettyCatalog = prettify ? JSON.stringify(JSON.parse(catalogText), null, 2) : catalogText; 104 | swal({ 105 | title: 'Edit catalog', 106 | className: 'edit-catalog', 107 | content: { 108 | element: 'textarea', 109 | attributes: { 110 | value: prettyCatalog 111 | } 112 | }, 113 | buttons: { 114 | cancel: { 115 | text: 'Cancel', 116 | visible: true 117 | }, 118 | confirm: { 119 | text: 'Update', 120 | visible: true, 121 | closeModal: false 122 | } 123 | } 124 | }).then((result) => { 125 | if (!result) { 126 | return; 127 | } 128 | let catalogData = $('.swal-modal.edit-catalog textarea').val(); 129 | jQuery.post('/admin/updateCatalog', 130 | { 131 | catalog: catalogData 132 | }, 133 | function() { 134 | swal({ 135 | title: 'Yay', 136 | text: 'The catalog has been updated.', 137 | icon: 'success' 138 | }).then(function() { 139 | refreshPage(); 140 | }); 141 | } 142 | ).fail(function(error, data) { 143 | let catalogData = $('.swal-modal.edit-catalog textarea').val(); 144 | swal({ 145 | title: 'Update failed', 146 | text: error.responseText, 147 | icon: 'error' 148 | }).then(result => { 149 | editCatalog(catalogData, false); 150 | }); 151 | }); 152 | }); 153 | } 154 | 155 | function refreshPage() { 156 | location.reload(); 157 | } 158 | -------------------------------------------------------------------------------- /views/style.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | cursor: pointer; 3 | } 4 | 5 | h6 { 6 | margin: 0px; 7 | } 8 | 9 | pre { 10 | padding: 10px; 11 | margin: 0px; 12 | color: #999; 13 | white-space: pre-wrap; 14 | } 15 | 16 | .instance p { 17 | font-size: 10px; 18 | margin-bottom: 0px; 19 | } 20 | 21 | .instance .card-header { 22 | background: #555; 23 | color: white; 24 | } 25 | 26 | .instance img { 27 | width: 100px; 28 | } 29 | 30 | pre .string { color: #690; } 31 | pre .number { color: #905; } 32 | pre .boolean { color: #905; } 33 | pre .null { color: #e90; } 34 | pre .key { color: #07a; } 35 | 36 | .swal-modal.edit-catalog { 37 | height: 90%; 38 | width: 90%; 39 | } 40 | 41 | .swal-modal.edit-catalog .swal-content__textarea { 42 | height: 70%; 43 | background: #333; 44 | color: #fff; 45 | font: 14px 'Courier New'; 46 | line-height: 1.6em; 47 | padding: 15px; 48 | border: none; 49 | } 50 | 51 | @keyframes spin { 52 | 0% { transform: rotate(0deg); } 53 | 100% { transform: rotate(360deg); } 54 | } 55 | 56 | .spin 57 | { 58 | animation: spin 8s linear infinite; 59 | } 60 | 61 | @-webkit-keyframes rainbow { 62 | 0%{background-position:0% 82%} 63 | 50%{background-position:100% 19%} 64 | 100%{background-position:0% 82%} 65 | } 66 | @-moz-keyframes rainbow { 67 | 0%{background-position:0% 82%} 68 | 50%{background-position:100% 19%} 69 | 100%{background-position:0% 82%} 70 | } 71 | @-o-keyframes rainbow { 72 | 0%{background-position:0% 82%} 73 | 50%{background-position:100% 19%} 74 | 100%{background-position:0% 82%} 75 | } 76 | @keyframes rainbow { 77 | 0%{background-position:0% 82%} 78 | 50%{background-position:100% 19%} 79 | 100%{background-position:0% 82%} 80 | } 81 | 82 | .instance .card-header.rainbow { 83 | background: linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3); 84 | background-size: 1800% 1800%; 85 | -webkit-animation: rainbow 18s ease infinite; 86 | -z-animation: rainbow 18s ease infinite; 87 | -o-animation: rainbow 18s ease infinite; 88 | animation: rainbow 18s ease infinite;} 89 | } 90 | --------------------------------------------------------------------------------