├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── manifest.yml ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── cloudfoundry │ │ └── community │ │ └── servicebroker │ │ └── postgresql │ │ ├── config │ │ ├── Application.java │ │ └── BrokerConfiguration.java │ │ └── service │ │ ├── Database.java │ │ ├── PostgreSQLDatabase.java │ │ ├── PostgreSQLServiceInstanceBindingService.java │ │ ├── PostgreSQLServiceInstanceService.java │ │ ├── Role.java │ │ └── Utils.java └── resources │ └── application.properties └── test ├── java └── org │ └── cloudfoundry │ └── community │ └── servicebroker │ ├── ServiceBrokerV2IntegrationTestBase.java │ └── postgresql │ └── PostgreSQLServiceBrokerV2IntegrationTests.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .project 3 | .classpath 4 | .settings 5 | .idea/ 6 | *.iml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | - oraclejdk7 5 | - openjdk7 6 | addons: 7 | postgresql: "9.4" 8 | before_script: 9 | - psql -c 'create database travis_ci_test;' -U postgres 10 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Foundry Service Broker for a PostgreSQL instance [![Build Status](https://travis-ci.org/cloudfoundry-community/postgresql-cf-service-broker.svg?branch=master)](https://travis-ci.org/cloudfoundry-community/postgresql-cf-service-broker) 2 | 3 | A Cloud Foundry Service Broker for a PostgreSQL instance built based on [s3-cf-service-broker](https://github.com/cloudfoundry-community/s3-cf-service-broker). 4 | 5 | The broker currently publishes a single service and plan for provisioning PostgreSQL databases. 6 | 7 | ## Design 8 | 9 | The broker uses a PostgreSQL table for it's meta data. It does not maintain an internal database so it has no dependencies besides PostgreSQL. 10 | 11 | Capability with the Cloud Foundry service broker API is indicated by the project version number. For example, version 2.4.0 is based off the 2.4 version of the broker API. 12 | 13 | ## Running 14 | 15 | Simply run the JAR file and provide a PostgreSQL jdbc url via the `MASTER_JDBC_URL` environment variable. 16 | 17 | ### Locally 18 | 19 | ``` 20 | mvn package && MASTER_JDBC_URL=jdbcurl java -jar target/postgresql-cf-service-broker-2.4.0-SNAPSHOT.jar 21 | ``` 22 | 23 | ### In Cloud Foundry 24 | 25 | Find out the database subnet and create a security group rule (postgresql.json): 26 | ``` 27 | [{"protocol":"tcp","destination":"10.10.8.0/24","ports":"5432"}] 28 | ``` 29 | 30 | import this into CF with: 31 | ``` 32 | cf create-security-group postgresql-service postgresql.json 33 | ``` 34 | 35 | Bind to the full cf install: 36 | ``` 37 | cf bind-running-security-group postgresql-service 38 | ``` 39 | 40 | 41 | Build the package with `mvn package` then push it out: 42 | ``` 43 | cf push postgresql-cf-service-broker -p target/postgresql-cf-service-broker-2.4.0-SNAPSHOT.jar --no-start 44 | ``` 45 | 46 | Export the following environment variables: 47 | 48 | ``` 49 | cf set-env postgresql-cf-service-broker MASTER_JDBC_URL "jdbcurl" 50 | cf set-env postgresql-cf-service-broker JAVA_OPTS "-Dsecurity.user.password=mysecret" 51 | ``` 52 | 53 | Start the service broker: 54 | ``` 55 | cf start postgresql-cf-service-broker 56 | ``` 57 | 58 | Create Cloud Foundry service broker: 59 | ``` 60 | cf create-service-broker postgresql-cf-service-broker user mysecret http://postgresql-cf-service-broker.cfapps.io 61 | ``` 62 | 63 | Add service broker to Cloud Foundry Marketplace: 64 | ``` 65 | cf enable-service-access PostgreSQL -p "Basic PostgreSQL Plan" -o ORG 66 | ``` 67 | 68 | ## Testing 69 | 70 | ### Locally 71 | 72 | You need to have a running PostgreSQL 9.x instance for this to work locally. 73 | To create an PostgreSQL database matching the ```MASTER_JDBC_URL``` in ```src/test/resources/application.properties```: 74 | ``` 75 | docker run -p 5432:5432/tcp --name testpostgres --rm -e POSTGRES_DB=travis_ci_test -e POSTGRES_PASSWORD= -e POSTGRES_USER=postgres postgres 76 | ``` 77 | 78 | Then run: 79 | ``` 80 | mvn test 81 | ``` 82 | 83 | ### In Travis CI 84 | The configuration for the test can be found in ```src/test/resources/application.properties``` and should match the configuration in Travis CI (```.travis.yml```). 85 | 86 | ## Using the services in your application 87 | 88 | ### Format of Credentials 89 | 90 | The credentials provided in a bind call have the following format: 91 | 92 | ``` 93 | "credentials":{ 94 | "uri":"postgres://__username__:__password__@__hostname__:__port__/__database__" 95 | } 96 | ``` 97 | 98 | ## Broker Security 99 | 100 | [spring-boot-starter-security](https://github.com/spring-projects/spring-boot/tree/master/spring-boot-starters/spring-boot-starter-security) is used. See the documentation here for configuration: [Spring boot security](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-security) 101 | 102 | The default password configured is "password" 103 | 104 | ## Creation of PostgreSQL databases 105 | 106 | A service provisioning call will create a PostgreSQL database. A binding call will return a database uri that can be used to connect to the database. Unbinding calls will disable the database user role and deprovisioning calls will delete all resources created. 107 | 108 | ## User for Broker 109 | 110 | An PostgreSQL user must be created for the broker. The username and password must be provided using the environment variable `MASTER_JDBC_URL`. 111 | 112 | ## Registering a Broker with the Cloud Controller 113 | 114 | See [Managing Service Brokers](http://docs.cloudfoundry.org/services/managing-service-brokers.html). 115 | 116 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: postgresql-cf-service-broker 4 | buildpack: java_buildpack 5 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | org.springframework.boot 5 | spring-boot-starter-parent 6 | 1.2.6.RELEASE 7 | 8 | 4.0.0 9 | net.mendix.broker 10 | postgresql-cf-service-broker 11 | 2.4.0-SNAPSHOT 12 | 13 | 14 | org.cloudfoundry.community.servicebroker.postgresql.config.Application 15 | 2.6.0 16 | 17 | 18 | 19 | 20 | org.postgresql 21 | postgresql 22 | 9.4-1206-jdbc41 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-logging 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-test 37 | test 38 | 39 | 40 | org.cloudfoundry 41 | spring-boot-cf-service-broker 42 | ${spring-boot-cf-service-broker.version} 43 | 44 | 45 | org.cloudfoundry 46 | spring-boot-cf-service-broker-tests 47 | ${spring-boot-cf-service-broker.version} 48 | test 49 | 50 | 51 | com.amazonaws 52 | aws-java-sdk 53 | 1.9.40 54 | 55 | 56 | joda-time 57 | joda-time 58 | 2.8.1 59 | 60 | 61 | com.google.guava 62 | guava 63 | 18.0 64 | 65 | 66 | org.slf4j 67 | slf4j-api 68 | 1.7.12 69 | 70 | 71 | com.jayway.restassured 72 | rest-assured 73 | 2.4.1 74 | test 75 | 76 | 77 | 78 | 79 | 80 | 81 | maven-compiler-plugin 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-surefire-plugin 90 | 91 | alphabetical 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | spring-io 100 | http://repo.spring.io/libs-snapshot 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/config/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.config; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 20 | import org.springframework.context.annotation.ComponentScan; 21 | 22 | @ComponentScan 23 | @EnableAutoConfiguration 24 | public class Application { 25 | 26 | public static void main(String[] args) { 27 | SpringApplication.run(Application.class, args); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/config/BrokerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.config; 17 | 18 | import org.cloudfoundry.community.servicebroker.config.BrokerApiVersionConfig; 19 | import org.cloudfoundry.community.servicebroker.model.Catalog; 20 | import org.cloudfoundry.community.servicebroker.model.Plan; 21 | import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | import org.springframework.beans.factory.annotation.Value; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.context.annotation.ComponentScan; 27 | import org.springframework.context.annotation.Configuration; 28 | import org.springframework.context.annotation.FilterType; 29 | 30 | import java.io.IOException; 31 | import java.sql.Connection; 32 | import java.sql.DriverManager; 33 | import java.sql.SQLException; 34 | import java.sql.Statement; 35 | import java.util.Arrays; 36 | import java.util.HashMap; 37 | import java.util.List; 38 | import java.util.Map; 39 | 40 | @Configuration 41 | @ComponentScan(basePackages = "org.cloudfoundry.community.servicebroker", excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = BrokerApiVersionConfig.class) }) 42 | public class BrokerConfiguration { 43 | 44 | private static final Logger logger = LoggerFactory.getLogger(BrokerConfiguration.class); 45 | 46 | @Value("${MASTER_JDBC_URL}") 47 | private String jdbcUrl; 48 | 49 | @Bean 50 | public Connection jdbc() { 51 | try { 52 | Connection conn = DriverManager.getConnection(this.jdbcUrl); 53 | 54 | String serviceTable = "CREATE TABLE IF NOT EXISTS service (serviceinstanceid varchar(200) not null default ''," 55 | + " servicedefinitionid varchar(200) not null default ''," 56 | + " planid varchar(200) not null default ''," 57 | + " organizationguid varchar(200) not null default ''," 58 | + " spaceguid varchar(200) not null default '')"; 59 | 60 | Statement createServiceTable = conn.createStatement(); 61 | createServiceTable.execute(serviceTable); 62 | return conn; 63 | } catch (SQLException e) { 64 | logger.error("Error while creating initial 'service' table", e); 65 | return null; 66 | } 67 | } 68 | 69 | @Bean 70 | public Catalog catalog() throws IOException { 71 | ServiceDefinition serviceDefinition = new ServiceDefinition("pg", "PostgreSQL", "PostgreSQL on shared instance.", 72 | true, false, getPlans(), getTags(), getServiceDefinitionMetadata(), Arrays.asList("syslog_drain"), null); 73 | return new Catalog(Arrays.asList(serviceDefinition)); 74 | } 75 | 76 | private static List getTags() { 77 | return Arrays.asList("PostgreSQL", "Database storage"); 78 | } 79 | 80 | private static Map getServiceDefinitionMetadata() { 81 | Map sdMetadata = new HashMap(); 82 | sdMetadata.put("displayName", "PostgreSQL"); 83 | sdMetadata.put("imageUrl", "https://wiki.postgresql.org/images/3/30/PostgreSQL_logo.3colors.120x120.png"); 84 | sdMetadata.put("longDescription", "PostgreSQL Service"); 85 | sdMetadata.put("providerDisplayName", "PostgreSQL"); 86 | sdMetadata.put("documentationUrl", "http://mendix.com/postgresql"); 87 | sdMetadata.put("supportUrl", "https://support.mendix.com"); 88 | return sdMetadata; 89 | } 90 | 91 | private static List getPlans() { 92 | Plan basic = new Plan("postgresql-basic-plan", "Basic PostgreSQL Plan", 93 | "A PG plan providing a single database on a shared instance with limited storage.", getBasicPlanMetadata()); 94 | return Arrays.asList(basic); 95 | } 96 | 97 | private static Map getBasicPlanMetadata() { 98 | Map planMetadata = new HashMap(); 99 | planMetadata.put("bullets", getBasicPlanBullets()); 100 | return planMetadata; 101 | } 102 | 103 | private static List getBasicPlanBullets() { 104 | return Arrays.asList("Single PG database", "Limited storage", "Shared instance"); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/Database.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.service; 17 | 18 | import org.cloudfoundry.community.servicebroker.model.CreateServiceInstanceRequest; 19 | import org.cloudfoundry.community.servicebroker.model.ServiceInstance; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.stereotype.Component; 23 | 24 | import java.sql.*; 25 | import java.util.Collections; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | @Component 31 | public class Database { 32 | 33 | private static final Logger logger = LoggerFactory.getLogger(Database.class); 34 | 35 | public void createDatabaseForInstance(String instanceId, String serviceId, String planId, String organizationGuid, String spaceGuid) throws SQLException { 36 | Utils.checkValidUUID(instanceId); 37 | PostgreSQLDatabase.executeUpdate("CREATE DATABASE \"" + instanceId + "\" ENCODING 'UTF8'"); 38 | PostgreSQLDatabase.executeUpdate("REVOKE all on database \"" + instanceId + "\" from public"); 39 | 40 | Map parameterMap = new HashMap(); 41 | parameterMap.put(1, instanceId); 42 | parameterMap.put(2, serviceId); 43 | parameterMap.put(3, planId); 44 | parameterMap.put(4, organizationGuid); 45 | parameterMap.put(5, spaceGuid); 46 | 47 | PostgreSQLDatabase.executePreparedUpdate("INSERT INTO service (serviceinstanceid, servicedefinitionid, planid, organizationguid, spaceguid) VALUES (?, ?, ?, ?, ?)", parameterMap); 48 | } 49 | 50 | public void deleteDatabase(String instanceId) throws SQLException { 51 | Utils.checkValidUUID(instanceId); 52 | 53 | Map parameterMap = new HashMap(); 54 | parameterMap.put(1, instanceId); 55 | 56 | Map result = PostgreSQLDatabase.executeSelect("SELECT current_user"); 57 | String currentUser = null; 58 | 59 | if(result != null) { 60 | currentUser = result.get("current_user"); 61 | } 62 | 63 | if(currentUser == null) { 64 | logger.error("Current user for instance '" + instanceId + "' could not be found"); 65 | } 66 | 67 | PostgreSQLDatabase.executePreparedSelect("SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = ? AND pid <> pg_backend_pid()", parameterMap); 68 | PostgreSQLDatabase.executeUpdate("ALTER DATABASE \"" + instanceId + "\" OWNER TO \"" + currentUser + "\""); 69 | PostgreSQLDatabase.executeUpdate("DROP DATABASE IF EXISTS \"" + instanceId + "\""); 70 | PostgreSQLDatabase.executePreparedUpdate("DELETE FROM service WHERE serviceinstanceid=?", parameterMap); 71 | } 72 | 73 | public ServiceInstance findServiceInstance(String instanceId) throws SQLException { 74 | Utils.checkValidUUID(instanceId); 75 | 76 | Map parameterMap = new HashMap(); 77 | parameterMap.put(1, instanceId); 78 | 79 | Map result = PostgreSQLDatabase.executePreparedSelect("SELECT * FROM service WHERE serviceinstanceid = ?", parameterMap); 80 | 81 | String serviceDefinitionId = result.get("servicedefinitionid"); 82 | String organizationGuid = result.get("organizationguid"); 83 | String planId = result.get("planid"); 84 | String spaceGuid = result.get("spaceguid"); 85 | 86 | CreateServiceInstanceRequest wrapper = new CreateServiceInstanceRequest(serviceDefinitionId, planId, organizationGuid, spaceGuid).withServiceInstanceId(instanceId); 87 | return new ServiceInstance(wrapper); 88 | } 89 | 90 | // TODO needs to be implemented 91 | public List getAllServiceInstances() { 92 | return Collections.emptyList(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/PostgreSQLDatabase.java: -------------------------------------------------------------------------------- 1 | package org.cloudfoundry.community.servicebroker.postgresql.service; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.sql.*; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @Component 15 | public class PostgreSQLDatabase { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(PostgreSQLDatabase.class); 18 | 19 | private static Connection conn; 20 | 21 | private static String databaseHost; 22 | 23 | private static int databasePort; 24 | 25 | @Autowired 26 | public PostgreSQLDatabase(Connection conn) { 27 | PostgreSQLDatabase.conn = conn; 28 | 29 | try { 30 | String jdbcUrl = conn.getMetaData().getURL(); 31 | // Remove "jdbc:" prefix from the connection JDBC URL to create an URI out of it. 32 | String cleanJdbcUrl = jdbcUrl.replace("jdbc:", ""); 33 | 34 | URI uri = new URI(cleanJdbcUrl); 35 | PostgreSQLDatabase.databaseHost = uri.getHost(); 36 | PostgreSQLDatabase.databasePort = uri.getPort() == -1 ? 5432 : uri.getPort(); 37 | } catch (SQLException e) { 38 | throw new IllegalStateException("Unable to get DatabaseMetadata from Connection", e); 39 | } catch (URISyntaxException e) { 40 | throw new IllegalStateException("Unable to parse JDBC URI for Database Connection", e); 41 | } 42 | } 43 | 44 | 45 | public static void executeUpdate(String query) throws SQLException { 46 | Statement statement = conn.createStatement(); 47 | 48 | try { 49 | statement.execute(query); 50 | } catch (SQLException e) { 51 | logger.error("Error while executing SQL UPDATE query '" + query + "'", e); 52 | } finally { 53 | statement.close(); 54 | } 55 | } 56 | 57 | public static Map executeSelect(String query) throws SQLException { 58 | Statement statement = conn.createStatement(); 59 | 60 | try { 61 | ResultSet result = statement.executeQuery(query); 62 | return getResultMapFromResultSet(result); 63 | } catch (SQLException e) { 64 | logger.error("Error while executing SQL SELECT query '" + query + "'", e); 65 | return null; 66 | } finally { 67 | statement.close(); 68 | } 69 | } 70 | 71 | public static void executePreparedUpdate(String query, Map parameterMap) throws SQLException { 72 | if(parameterMap == null) { 73 | throw new IllegalStateException("parameterMap cannot be null"); 74 | } 75 | 76 | PreparedStatement preparedStatement = conn.prepareStatement(query); 77 | 78 | for(Map.Entry parameter : parameterMap.entrySet()) { 79 | preparedStatement.setString(parameter.getKey(), parameter.getValue()); 80 | } 81 | 82 | try { 83 | preparedStatement.executeUpdate(); 84 | } catch (SQLException e) { 85 | logger.error("Error while executing SQL prepared UPDATE query '" + query + "'", e); 86 | } finally { 87 | preparedStatement.close(); 88 | } 89 | } 90 | 91 | public static Map executePreparedSelect(String query, Map parameterMap) throws SQLException { 92 | if(parameterMap == null) { 93 | throw new IllegalStateException("parameterMap cannot be null"); 94 | } 95 | 96 | PreparedStatement preparedStatement = conn.prepareStatement(query); 97 | 98 | for(Map.Entry parameter : parameterMap.entrySet()) { 99 | preparedStatement.setString(parameter.getKey(), parameter.getValue()); 100 | } 101 | 102 | try { 103 | ResultSet result = preparedStatement.executeQuery(); 104 | return getResultMapFromResultSet(result); 105 | } catch (SQLException e) { 106 | logger.error("Error while executing SQL prepared SELECT query '" + query + "'", e); 107 | return null; 108 | } finally { 109 | preparedStatement.close(); 110 | } 111 | } 112 | 113 | public static String getDatabaseHost() { 114 | return databaseHost; 115 | } 116 | 117 | public static int getDatabasePort() { 118 | return databasePort; 119 | } 120 | 121 | private static Map getResultMapFromResultSet(ResultSet result) throws SQLException { 122 | ResultSetMetaData resultMetaData = result.getMetaData(); 123 | int columns = resultMetaData.getColumnCount(); 124 | 125 | Map resultMap = new HashMap(columns); 126 | 127 | if(result.next()) { 128 | for(int i = 1; i <= columns; i++) { 129 | resultMap.put(resultMetaData.getColumnName(i), result.getString(i)); 130 | } 131 | } 132 | 133 | return resultMap; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/PostgreSQLServiceInstanceBindingService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.service; 17 | 18 | import org.cloudfoundry.community.servicebroker.exception.ServiceBrokerException; 19 | import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceBindingExistsException; 20 | import org.cloudfoundry.community.servicebroker.model.CreateServiceInstanceBindingRequest; 21 | import org.cloudfoundry.community.servicebroker.model.DeleteServiceInstanceBindingRequest; 22 | import org.cloudfoundry.community.servicebroker.model.ServiceInstanceBinding; 23 | import org.cloudfoundry.community.servicebroker.service.ServiceInstanceBindingService; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.stereotype.Service; 28 | 29 | import java.sql.SQLException; 30 | import java.util.HashMap; 31 | import java.util.Map; 32 | 33 | @Service 34 | public class PostgreSQLServiceInstanceBindingService implements ServiceInstanceBindingService { 35 | 36 | private static final Logger logger = LoggerFactory.getLogger(PostgreSQLServiceInstanceBindingService.class); 37 | 38 | private final Role role; 39 | 40 | @Autowired 41 | public PostgreSQLServiceInstanceBindingService(Role role) { 42 | this.role = role; 43 | } 44 | 45 | @Override 46 | public ServiceInstanceBinding createServiceInstanceBinding(CreateServiceInstanceBindingRequest createServiceInstanceBindingRequest) 47 | throws ServiceInstanceBindingExistsException, ServiceBrokerException { 48 | String bindingId = createServiceInstanceBindingRequest.getBindingId(); 49 | String serviceInstanceId = createServiceInstanceBindingRequest.getServiceInstanceId(); 50 | String appGuid = createServiceInstanceBindingRequest.getAppGuid(); 51 | String passwd = ""; 52 | 53 | try { 54 | passwd = this.role.bindRoleToDatabase(serviceInstanceId); 55 | } catch (SQLException e) { 56 | logger.error("Error while creating service instance binding '" + bindingId + "'", e); 57 | throw new ServiceBrokerException(e.getMessage()); 58 | } 59 | 60 | String dbURL = String.format("postgres://%s:%s@%s:%d/%s", serviceInstanceId, passwd, PostgreSQLDatabase.getDatabaseHost(), PostgreSQLDatabase.getDatabasePort(), serviceInstanceId); 61 | 62 | Map credentials = new HashMap(); 63 | credentials.put("uri", dbURL); 64 | 65 | return new ServiceInstanceBinding(bindingId, serviceInstanceId, credentials, null, appGuid); 66 | } 67 | 68 | @Override 69 | public ServiceInstanceBinding deleteServiceInstanceBinding(DeleteServiceInstanceBindingRequest deleteServiceInstanceBindingRequest) 70 | throws ServiceBrokerException { 71 | String serviceInstanceId = deleteServiceInstanceBindingRequest.getInstance().getServiceInstanceId(); 72 | String bindingId = deleteServiceInstanceBindingRequest.getBindingId(); 73 | try { 74 | this.role.unBindRoleFromDatabase(serviceInstanceId); 75 | } catch (SQLException e) { 76 | logger.error("Error while deleting service instance binding '" + bindingId + "'", e); 77 | throw new ServiceBrokerException(e.getMessage()); 78 | } 79 | return new ServiceInstanceBinding(bindingId, serviceInstanceId, null, null, null); 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/PostgreSQLServiceInstanceService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.service; 17 | 18 | import org.cloudfoundry.community.servicebroker.exception.ServiceBrokerException; 19 | import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceDoesNotExistException; 20 | import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceExistsException; 21 | import org.cloudfoundry.community.servicebroker.exception.ServiceInstanceUpdateNotSupportedException; 22 | import org.cloudfoundry.community.servicebroker.model.CreateServiceInstanceRequest; 23 | import org.cloudfoundry.community.servicebroker.model.DeleteServiceInstanceRequest; 24 | import org.cloudfoundry.community.servicebroker.model.ServiceInstance; 25 | import org.cloudfoundry.community.servicebroker.model.UpdateServiceInstanceRequest; 26 | import org.cloudfoundry.community.servicebroker.service.ServiceInstanceService; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | import org.springframework.beans.factory.annotation.Autowired; 30 | import org.springframework.stereotype.Service; 31 | 32 | import java.sql.SQLException; 33 | 34 | @Service 35 | public class PostgreSQLServiceInstanceService implements ServiceInstanceService { 36 | 37 | private static final Logger logger = LoggerFactory.getLogger(PostgreSQLServiceInstanceService.class); 38 | 39 | private final Database db; 40 | 41 | private final Role role; 42 | 43 | @Autowired 44 | public PostgreSQLServiceInstanceService(Database db, Role role) { 45 | this.db = db; 46 | this.role = role; 47 | } 48 | 49 | @Override 50 | public ServiceInstance createServiceInstance(CreateServiceInstanceRequest createServiceInstanceRequest) 51 | throws ServiceInstanceExistsException, ServiceBrokerException { 52 | String serviceInstanceId = createServiceInstanceRequest.getServiceInstanceId(); 53 | String serviceId = createServiceInstanceRequest.getServiceDefinitionId(); 54 | String planId = createServiceInstanceRequest.getPlanId(); 55 | String organizationGuid = createServiceInstanceRequest.getOrganizationGuid(); 56 | String spaceGuid = createServiceInstanceRequest.getSpaceGuid(); 57 | try { 58 | db.createDatabaseForInstance(serviceInstanceId, serviceId, planId, organizationGuid, spaceGuid); 59 | role.createRoleForInstance(serviceInstanceId); 60 | } catch (SQLException e) { 61 | logger.error("Error while creating service instance '" + serviceInstanceId + "'", e); 62 | throw new ServiceBrokerException(e.getMessage()); 63 | } 64 | return new ServiceInstance(createServiceInstanceRequest); 65 | } 66 | 67 | @Override 68 | public ServiceInstance deleteServiceInstance(DeleteServiceInstanceRequest deleteServiceInstanceRequest) 69 | throws ServiceBrokerException { 70 | String serviceInstanceId = deleteServiceInstanceRequest.getServiceInstanceId(); 71 | ServiceInstance instance = getServiceInstance(serviceInstanceId); 72 | 73 | try { 74 | db.deleteDatabase(serviceInstanceId); 75 | role.deleteRole(serviceInstanceId); 76 | } catch (SQLException e) { 77 | logger.error("Error while deleting service instance '" + serviceInstanceId + "'", e); 78 | throw new ServiceBrokerException(e.getMessage()); 79 | } 80 | return instance; 81 | } 82 | 83 | @Override 84 | public ServiceInstance updateServiceInstance(UpdateServiceInstanceRequest updateServiceInstanceRequest) 85 | throws ServiceInstanceUpdateNotSupportedException, ServiceBrokerException, ServiceInstanceDoesNotExistException { 86 | throw new IllegalStateException("Not implemented"); 87 | } 88 | 89 | @Override 90 | public ServiceInstance getServiceInstance(String id) { 91 | try { 92 | return db.findServiceInstance(id); 93 | } catch (SQLException e) { 94 | logger.error("Error while finding service instance '" + id + "'", e); 95 | return null; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/Role.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.service; 17 | 18 | import org.springframework.stereotype.Component; 19 | 20 | import java.math.BigInteger; 21 | import java.security.SecureRandom; 22 | import java.sql.SQLException; 23 | 24 | @Component 25 | public class Role { 26 | 27 | public void createRoleForInstance(String instanceId) throws SQLException { 28 | Utils.checkValidUUID(instanceId); 29 | PostgreSQLDatabase.executeUpdate("CREATE ROLE \"" + instanceId + "\""); 30 | PostgreSQLDatabase.executeUpdate("ALTER DATABASE \"" + instanceId + "\" OWNER TO \"" + instanceId + "\""); 31 | } 32 | 33 | public void deleteRole(String instanceId) throws SQLException { 34 | Utils.checkValidUUID(instanceId); 35 | PostgreSQLDatabase.executeUpdate("DROP ROLE IF EXISTS \"" + instanceId + "\""); 36 | } 37 | 38 | public String bindRoleToDatabase(String dbInstanceId) throws SQLException { 39 | Utils.checkValidUUID(dbInstanceId); 40 | 41 | SecureRandom random = new SecureRandom(); 42 | String passwd = new BigInteger(130, random).toString(32); 43 | 44 | PostgreSQLDatabase.executeUpdate("ALTER ROLE \"" + dbInstanceId + "\" LOGIN password '" + passwd + "'"); 45 | return passwd; 46 | } 47 | 48 | public void unBindRoleFromDatabase(String dbInstanceId) throws SQLException{ 49 | Utils.checkValidUUID(dbInstanceId); 50 | PostgreSQLDatabase.executeUpdate("ALTER ROLE \"" + dbInstanceId + "\" NOLOGIN"); 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/java/org/cloudfoundry/community/servicebroker/postgresql/service/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.cloudfoundry.community.servicebroker.postgresql.service; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.util.UUID; 22 | 23 | public class Utils { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(Utils.class); 26 | 27 | private Utils() { 28 | } 29 | 30 | public static void checkValidUUID(String uuidString) { 31 | logger.info("Checking if this UUID string is a valid UUID: " + uuidString); 32 | UUID uuid = UUID.fromString(uuidString); 33 | 34 | if(!uuidString.equals(uuid.toString())) { 35 | throw new IllegalStateException("UUID '" + uuidString + "' is not an UUID."); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | security.user.password: password -------------------------------------------------------------------------------- /src/test/java/org/cloudfoundry/community/servicebroker/ServiceBrokerV2IntegrationTestBase.java: -------------------------------------------------------------------------------- 1 | package org.cloudfoundry.community.servicebroker; 2 | 3 | import com.jayway.restassured.RestAssured; 4 | import com.jayway.restassured.http.ContentType; 5 | import com.jayway.restassured.response.Header; 6 | import org.apache.http.HttpStatus; 7 | import org.junit.Before; 8 | import org.junit.BeforeClass; 9 | import org.junit.FixMethodOrder; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.MethodSorters; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.boot.test.IntegrationTest; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | import org.springframework.test.context.web.WebAppConfiguration; 17 | 18 | import java.util.UUID; 19 | 20 | import static com.jayway.restassured.RestAssured.given; 21 | 22 | /** 23 | * Abstract test base class for the Service Broker V2 API. 24 | * 25 | * Usage: 26 | * Annotate the implementing test class with the following implementation-specific annotation: 27 | * 28 | * @SpringApplicationConfiguration(classes = Application.class) 29 | * 30 | * If you would want to test the actual creation/deletion of resources, you might also want this annotation: 31 | * 32 | * @FixMethodOrder(MethodSorters.NAME_ASCENDING) 33 | * 34 | * This would cause JUnit to run the methods in name-ascending order, causing the cases to run in order. 35 | */ 36 | @RunWith(SpringJUnit4ClassRunner.class) 37 | @WebAppConfiguration 38 | @IntegrationTest("server.port:0") 39 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 40 | public abstract class ServiceBrokerV2IntegrationTestBase { 41 | 42 | @Value("${local.server.port}") 43 | protected int port; 44 | 45 | @Value("${security.user.password}") 46 | protected String password; 47 | 48 | @Value("${service_id}") 49 | protected String serviceId; 50 | 51 | @Value("${plan_id}") 52 | protected String planId; 53 | 54 | protected final String username = "user"; 55 | 56 | protected final String organizationGuid = "system"; 57 | 58 | protected final String spaceGuid = "thespace"; 59 | 60 | protected static String instanceId; 61 | 62 | protected static String appGuid; 63 | 64 | protected final String fetchCatalogPath = "/v2/catalog"; 65 | 66 | protected final String provisionOrRemoveInstanceBasePath = "/v2/service_instances/%s"; 67 | 68 | protected final String createOrRemoveBindingBasePath = "/v2/service_instances/%s/service_bindings/%s"; 69 | 70 | protected final Header apiVersionHeader = new Header("X-Broker-Api-Version", BROKER_API_VERSION); 71 | 72 | public static final String BROKER_API_VERSION = "2.4"; 73 | 74 | @Before 75 | public void setUp() throws Exception { 76 | RestAssured.port = port; 77 | } 78 | 79 | @BeforeClass 80 | public static void generateUniqueIds() { 81 | instanceId = UUID.randomUUID().toString(); 82 | appGuid = UUID.randomUUID().toString(); 83 | } 84 | 85 | /** 86 | * cf marketplace 87 | * cf create-service-broker 88 | *

89 | * Fetch Catalog (GET /v2/catalog) 90 | */ 91 | 92 | @Test 93 | public void case1_fetchCatalogFailsWithoutCredentials() throws Exception { 94 | given().auth().none().when().get(fetchCatalogPath).then().statusCode(HttpStatus.SC_UNAUTHORIZED); 95 | } 96 | 97 | @Test 98 | public void case1_fetchCatalogSucceedsWithCredentials() throws Exception { 99 | given().auth().basic(username, password).header(apiVersionHeader).when().get(fetchCatalogPath).then().statusCode(HttpStatus.SC_OK); 100 | } 101 | 102 | /** 103 | * cf create-service 104 | *

105 | * Provision Instance (PUT /v2/service_instances/:id) 106 | */ 107 | 108 | @Test 109 | public void case2_provisionInstanceFailsWithoutCredentials() throws Exception { 110 | String provisionInstancePath = String.format(provisionOrRemoveInstanceBasePath, instanceId); 111 | given().auth().none().when().put(provisionInstancePath).then().statusCode(HttpStatus.SC_UNAUTHORIZED); 112 | } 113 | 114 | @Test 115 | public void case2_provisionInstanceSucceedsWithCredentials() throws Exception { 116 | String provisionInstancePath = String.format(provisionOrRemoveInstanceBasePath, instanceId); 117 | String request_body = "{\n" + 118 | " \"service_id\": \"" + serviceId + "\",\n" + 119 | " \"plan_id\": \"" + planId + "\",\n" + 120 | " \"organization_guid\": \"" + organizationGuid + "\",\n" + 121 | " \"space_guid\": \"" + spaceGuid + "\"\n" + 122 | "}"; 123 | 124 | given().auth().basic(username, password).header(apiVersionHeader).request().contentType(ContentType.JSON).body(request_body).when().put(provisionInstancePath).then().statusCode(HttpStatus.SC_CREATED); 125 | } 126 | 127 | /** 128 | * cf bind-service 129 | *

130 | * Create Binding (PUT /v2/service_instances/:instance_id/service_bindings/:id) 131 | */ 132 | 133 | @Test 134 | public void case3_createBindingFailsWithoutCredentials() throws Exception { 135 | String createBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId); 136 | given().auth().none().when().put(createBindingPath).then().statusCode(HttpStatus.SC_UNAUTHORIZED); 137 | } 138 | 139 | @Test 140 | public void case3_createBindingSucceedsWithCredentials() throws Exception { 141 | String createBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId); 142 | String request_body = "{\n" + 143 | " \"plan_id\": \"" + planId + "\",\n" + 144 | " \"service_id\": \"" + serviceId + "\",\n" + 145 | " \"app_guid\": \"" + appGuid + "\"\n" + 146 | "}"; 147 | 148 | given().auth().basic(username, password).header(apiVersionHeader).request().contentType(ContentType.JSON).body(request_body).when().put(createBindingPath).then().statusCode(HttpStatus.SC_CREATED); 149 | } 150 | 151 | /** 152 | * cf unbind-service 153 | *

154 | * Remove Binding (DELETE /v2/service_instances/:instance_id/service_bindings/:id) 155 | */ 156 | 157 | @Test 158 | public void case4_removeBindingFailsWithoutCredentials() throws Exception { 159 | String removeBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId) + "?service_id=" + serviceId + "&plan_id=" + planId; 160 | given().auth().none().when().delete(removeBindingPath).then().statusCode(HttpStatus.SC_UNAUTHORIZED); 161 | } 162 | 163 | @Test 164 | public void case4_removeBindingSucceedsWithCredentials() throws Exception { 165 | String removeBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId) + "?service_id=" + serviceId + "&plan_id=" + planId; 166 | given().auth().basic(username, password).header(apiVersionHeader).when().delete(removeBindingPath).then().statusCode(HttpStatus.SC_OK); 167 | } 168 | 169 | /** 170 | * cf delete-service 171 | *

172 | * Remove Instance (DELETE /v2/service_instances/:id) 173 | */ 174 | 175 | @Test 176 | public void case5_removeInstanceFailsWithoutCredentials() throws Exception { 177 | String removeInstancePath = String.format(provisionOrRemoveInstanceBasePath, instanceId) + "?service_id=" + serviceId + "&plan_id=" + planId; 178 | given().auth().none().when().delete(removeInstancePath).then().statusCode(HttpStatus.SC_UNAUTHORIZED); 179 | } 180 | 181 | @Test 182 | public void case5_removeInstanceSucceedsWithCredentials() throws Exception { 183 | String removeInstancePath = String.format(provisionOrRemoveInstanceBasePath, instanceId) + "?service_id=" + serviceId + "&plan_id=" + planId; 184 | given().auth().basic(username, password).header(apiVersionHeader).when().delete(removeInstancePath).then().statusCode(HttpStatus.SC_OK); 185 | } 186 | } -------------------------------------------------------------------------------- /src/test/java/org/cloudfoundry/community/servicebroker/postgresql/PostgreSQLServiceBrokerV2IntegrationTests.java: -------------------------------------------------------------------------------- 1 | package org.cloudfoundry.community.servicebroker.postgresql; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.jayway.restassured.http.ContentType; 5 | import com.jayway.restassured.response.ValidatableResponse; 6 | import org.apache.http.HttpStatus; 7 | import org.cloudfoundry.community.servicebroker.ServiceBrokerV2IntegrationTestBase; 8 | import org.cloudfoundry.community.servicebroker.model.Plan; 9 | import org.cloudfoundry.community.servicebroker.model.ServiceDefinition; 10 | import org.cloudfoundry.community.servicebroker.postgresql.config.Application; 11 | import org.cloudfoundry.community.servicebroker.postgresql.config.BrokerConfiguration; 12 | import org.cloudfoundry.community.servicebroker.postgresql.service.PostgreSQLDatabase; 13 | import org.junit.Before; 14 | import org.junit.FixMethodOrder; 15 | import org.junit.Test; 16 | import org.junit.runners.MethodSorters; 17 | import org.springframework.beans.factory.annotation.Value; 18 | import org.springframework.boot.test.SpringApplicationConfiguration; 19 | 20 | import java.sql.Connection; 21 | import java.sql.DatabaseMetaData; 22 | import java.sql.DriverManager; 23 | import java.sql.ResultSet; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | import static com.jayway.restassured.RestAssured.given; 29 | import static org.hamcrest.CoreMatchers.*; 30 | import static org.junit.Assert.*; 31 | 32 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 33 | @SpringApplicationConfiguration(classes = Application.class) 34 | public class PostgreSQLServiceBrokerV2IntegrationTests extends ServiceBrokerV2IntegrationTestBase { 35 | 36 | @Value("${MASTER_JDBC_URL}") 37 | private String jdbcUrl; 38 | 39 | private Connection conn; 40 | 41 | @Override 42 | @Before 43 | public void setUp() throws Exception { 44 | super.setUp(); 45 | conn = DriverManager.getConnection(this.jdbcUrl); 46 | } 47 | 48 | /** 49 | * Sanity check, to make sure that the 'service' table required for this Service Broker is created. 50 | */ 51 | 52 | @Test 53 | public void case0_checkThatServiceTableIsCreated() throws Exception { 54 | assertTrue(checkTableExists("service")); 55 | } 56 | 57 | /** 58 | * cf marketplace 59 | * cf create-service-broker 60 | *

61 | * Fetch Catalog (GET /v2/catalog) 62 | */ 63 | 64 | @Override 65 | @Test 66 | public void case1_fetchCatalogSucceedsWithCredentials() throws Exception { 67 | // same as super code, but we need the response here 68 | ValidatableResponse response = given().auth().basic(username, password).header(apiVersionHeader).when().get(fetchCatalogPath).then().statusCode(HttpStatus.SC_OK); 69 | 70 | BrokerConfiguration brokerConfiguration = new BrokerConfiguration(); 71 | ServiceDefinition serviceDefinition = brokerConfiguration.catalog().getServiceDefinitions().get(0); 72 | 73 | response.body("services[0].id", equalTo(serviceDefinition.getId())); 74 | response.body("services[0].name", equalTo(serviceDefinition.getName())); 75 | response.body("services[0].description", equalTo(serviceDefinition.getDescription())); 76 | response.body("services[0].requires", equalTo(serviceDefinition.getRequires())); 77 | response.body("services[0].tags", equalTo(serviceDefinition.getTags())); 78 | 79 | List planIds = new ArrayList(); 80 | for(Plan plan: serviceDefinition.getPlans()) { 81 | planIds.add(plan.getId()); 82 | } 83 | response.body("services[0].plans.id", equalTo(planIds)); 84 | } 85 | 86 | /** 87 | * cf create-service 88 | *

89 | * Provision Instance (PUT /v2/service_instances/:id) 90 | */ 91 | 92 | @Override 93 | @Test 94 | public void case2_provisionInstanceSucceedsWithCredentials() throws Exception { 95 | super.case2_provisionInstanceSucceedsWithCredentials(); 96 | 97 | assertTrue(checkDatabaseExists(instanceId)); 98 | assertTrue(checkRoleExists(instanceId)); 99 | assertTrue(checkRoleIsDatabaseOwner(instanceId, instanceId)); 100 | 101 | Map serviceResult = PostgreSQLDatabase.executePreparedSelect("SELECT * FROM service WHERE serviceinstanceid = ?", ImmutableMap.of(1, instanceId)); 102 | assertThat(serviceResult.get("organizationguid"), is(organizationGuid)); 103 | assertThat(serviceResult.get("planid"), is(planId)); 104 | assertThat(serviceResult.get("spaceguid"), is(spaceGuid)); 105 | assertThat(serviceResult.get("servicedefinitionid"), is(serviceId)); 106 | assertThat(serviceResult.get("serviceinstanceid"), is(instanceId)); 107 | } 108 | 109 | /** 110 | * cf bind-service 111 | *

112 | * Create Binding (PUT /v2/service_instances/:instance_id/service_bindings/:id) 113 | */ 114 | 115 | @Override 116 | @Test 117 | public void case3_createBindingSucceedsWithCredentials() throws Exception { 118 | // same as super code, but we need the response here 119 | String createBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId); 120 | String request_body = "{\n" + 121 | " \"plan_id\": \"" + planId + "\",\n" + 122 | " \"service_id\": \"" + serviceId + "\",\n" + 123 | " \"app_guid\": \"" + appGuid + "\"\n" + 124 | "}"; 125 | 126 | ValidatableResponse response = given().auth().basic(username, password).header(apiVersionHeader).request().contentType(ContentType.JSON).body(request_body).when().put(createBindingPath).then().statusCode(HttpStatus.SC_CREATED); 127 | 128 | response.body("credentials.uri", containsString("postgres://" + instanceId)); 129 | response.body("syslog_drain_url", is(nullValue())); 130 | } 131 | 132 | /** 133 | * cf unbind-service 134 | *

135 | * Remove Binding (DELETE /v2/service_instances/:instance_id/service_bindings/:id) 136 | */ 137 | 138 | @Override 139 | @Test 140 | public void case4_removeBindingSucceedsWithCredentials() throws Exception { 141 | // same as super code, but we need the response here 142 | String removeBindingPath = String.format(createOrRemoveBindingBasePath, instanceId, serviceId) + "?service_id=" + serviceId + "&plan_id=" + planId; 143 | ValidatableResponse response = given().auth().basic(username, password).header(apiVersionHeader).when().delete(removeBindingPath).then().statusCode(HttpStatus.SC_OK); 144 | 145 | // response body is empty json 146 | response.body(equalTo("{}")); 147 | } 148 | 149 | /** 150 | * cf delete-service 151 | *

152 | * Remove Instance (DELETE /v2/service_instances/:id) 153 | */ 154 | 155 | @Override 156 | @Test 157 | public void case5_removeInstanceSucceedsWithCredentials() throws Exception { 158 | super.case5_removeInstanceSucceedsWithCredentials(); 159 | 160 | assertFalse(checkDatabaseExists(instanceId)); 161 | assertFalse(checkRoleExists(instanceId)); 162 | assertFalse(checkRoleIsDatabaseOwner(instanceId, instanceId)); 163 | 164 | Map serviceResult = PostgreSQLDatabase.executePreparedSelect("SELECT * FROM service WHERE serviceinstanceid = ?", ImmutableMap.of(1, instanceId)); 165 | assertTrue(serviceResult.isEmpty()); 166 | } 167 | 168 | private boolean checkTableExists(String tableName) throws Exception { 169 | DatabaseMetaData md = conn.getMetaData(); 170 | ResultSet rs = md.getTables(null, null, tableName, null); 171 | 172 | // ResultSet.last() followed by ResultSet.getRow() will give you the row count 173 | rs.last(); 174 | int rowCount = rs.getRow(); 175 | return rowCount == 1; 176 | } 177 | 178 | private boolean checkDatabaseExists(String databaseName) throws Exception { 179 | Map pgDatabaseResult = PostgreSQLDatabase.executePreparedSelect("SELECT * FROM pg_catalog.pg_database WHERE datname = ?", ImmutableMap.of(1, databaseName)); 180 | return pgDatabaseResult.size() > 0; 181 | } 182 | 183 | private boolean checkRoleExists(String roleName) throws Exception { 184 | Map pgRoleResult = PostgreSQLDatabase.executePreparedSelect("SELECT * FROM pg_catalog.pg_roles WHERE rolname = ?", ImmutableMap.of(1, roleName)); 185 | return pgRoleResult.size() > 0; 186 | } 187 | 188 | private boolean checkRoleIsDatabaseOwner(String roleName, String databaseName) throws Exception { 189 | Map pgRoleIsDatabaseOwnerResult = PostgreSQLDatabase.executePreparedSelect("SELECT d.datname as name, pg_catalog.pg_get_userbyid(d.datdba) as owner FROM pg_catalog.pg_database d WHERE d.datname = ?", ImmutableMap.of(1, databaseName)); 190 | String owner = pgRoleIsDatabaseOwnerResult.get("owner"); 191 | return (owner != null) ? owner.equals(roleName) : false; 192 | } 193 | } -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | security.user.password: password 2 | service_id: pg 3 | plan_id: postgresql-basic-plan 4 | MASTER_JDBC_URL: jdbc:postgresql://localhost:5432/travis_ci_test?user=postgres&password= --------------------------------------------------------------------------------