Skip to content

Commit 64620b9

Browse files
authored
Merge pull request #5 from squidmin/google-cloud-sql-endpoints
Add google cloud sql endpoints
2 parents a631fca + ee8a4fe commit 64620b9

File tree

13 files changed

+312
-39
lines changed

13 files changed

+312
-39
lines changed

google-cloud-sql/docs/cloud-sql-auth-proxy.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
# Cloud SQL Auth Proxy
22

3-
### Set instance connection name
3+
### Set environment variables
44

55
```bash
6+
export CREDENTIALS_PATH="/Users/admin/.config/gcloud/sa-private-key.json"
7+
export GOOGLE_APPLICATION_CREDENTIALS="$CREDENTIALS_PATH"
8+
export TF_VAR_project_id=$(gcloud config get-value project)
9+
export TF_VAR_region="us-central1"
10+
export TF_VAR_authorized_cidr="$(curl -4 -s ifconfig.me)/32"
11+
export TF_VAR_db_user=username
12+
export TF_VAR_db_password=password
13+
export TF_VAR_db_name=jm_demo_db
614
export INSTANCE_CONNECTION_NAME="lofty-root-378503:us-central1:jm-pg-demo"
15+
export PGPASSWORD="$TF_VAR_db_password"
716
```
817

18+
### Provision DB infrastructure
19+
20+
Use Terraform or `gcloud` to create the Cloud SQL Postgres instance.
21+
922
### Run the CloudSQL auth proxy
1023

1124
Remove any old proxy container:
@@ -28,8 +41,6 @@ PROXY_PID=$!
2841
#### Docker
2942

3043
```bash
31-
32-
export CREDENTIALS_PATH="/Users/admin/.config/gcloud/sa-private-key.json"
3344
docker run -d --name csql-proxy -p 127.0.0.1:5400:5432 \
3445
-v "$GOOGLE_APPLICATION_CREDENTIALS:/creds/key.json:ro" \
3546
gcr.io/cloud-sql-connectors/cloud-sql-proxy:2 \
@@ -89,12 +100,6 @@ for p in {5400..5450}; do (lsof -iTCP:$p -sTCP:LISTEN >/dev/null) || { echo "Fir
89100

90101
### Test connection
91102

92-
```bash
93-
export TF_VAR_db_user=username
94-
export TF_VAR_db_password=password
95-
export TF_VAR_db_name=jm_demo_db
96-
```
97-
98103
Have the proxy running and healthy to `127.0.0.1:5432`.
99104
Make sure nothing else is using port `5432`.
100105
If it is, pick another port (e.g., 5433) and update your `psql` command.
@@ -110,7 +115,7 @@ PGPASSWORD="$TF_VAR_db_password" psql \
110115

111116
### If the test is successful
112117

113-
Assuming the file `/src/test/resources/init.sql` exists:
118+
Create the table from file contents. For example, assuming `/src/test/resources/init.sql` exists:
114119

115120
```bash
116121
PGPASSWORD="$TF_VAR_db_password" psql \
@@ -205,6 +210,10 @@ DROP TABLE widgets;
205210

206211
### Exit `psql`
207212

213+
```psql
214+
\q
215+
```
216+
208217
```psql
209218
exit
210219
```

google-cloud-sql/pom.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
<groupId>org.springframework.boot</groupId>
6161
<artifactId>spring-boot-starter-web</artifactId>
6262
</dependency>
63+
<dependency>
64+
<groupId>org.springframework.boot</groupId>
65+
<artifactId>spring-boot-starter-data-jpa</artifactId>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.springframework.boot</groupId>
69+
<artifactId>spring-boot-starter-jdbc</artifactId>
70+
</dependency>
6371

6472
<dependency>
6573
<groupId>org.springframework.cloud</groupId>
@@ -78,6 +86,18 @@
7886
<optional>true</optional>
7987
</dependency>
8088

89+
<dependency>
90+
<groupId>org.postgresql</groupId>
91+
<artifactId>postgresql</artifactId>
92+
<scope>runtime</scope>
93+
</dependency>
94+
95+
<!-- Hibernate -->
96+
<dependency>
97+
<groupId>org.hibernate.orm</groupId>
98+
<artifactId>hibernate-core</artifactId>
99+
</dependency>
100+
81101
<dependency>
82102
<groupId>org.projectlombok</groupId>
83103
<artifactId>lombok</artifactId>
@@ -91,6 +111,11 @@
91111
<optional>true</optional>
92112
</dependency>
93113

114+
<dependency>
115+
<groupId>com.h2database</groupId>
116+
<artifactId>h2</artifactId>
117+
<scope>test</scope>
118+
</dependency>
94119
<dependency>
95120
<groupId>org.springframework.boot</groupId>
96121
<artifactId>spring-boot-starter-test</artifactId>
@@ -110,6 +135,12 @@
110135

111136
<dependencyManagement>
112137
<dependencies>
138+
<dependency>
139+
<groupId>org.postgresql</groupId>
140+
<artifactId>postgresql</artifactId>
141+
<version>42.7.7</version>
142+
<scope>runtime</scope>
143+
</dependency>
113144
<dependency>
114145
<groupId>com.google.cloud</groupId>
115146
<artifactId>spring-cloud-gcp-dependencies</artifactId>

google-cloud-sql/src/main/java/org/squidmin/java/spring/maven/cloudsql/controller/CloudSqlController.java

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.squidmin.java.spring.maven.cloudsql.controller;
2+
3+
import org.springframework.http.MediaType;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.*;
6+
import org.squidmin.java.spring.maven.cloudsql.service.WidgetService;
7+
import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
8+
9+
import java.util.List;
10+
import java.util.UUID;
11+
12+
@RestController
13+
@RequestMapping("/cloud-sql/api/widgets")
14+
public class WidgetController {
15+
16+
private final WidgetService widgetService;
17+
18+
public WidgetController(WidgetService widgetService) {
19+
this.widgetService = widgetService;
20+
}
21+
22+
@GetMapping
23+
public List<Widget> list(@RequestParam(name = "q", required = false) String q) {
24+
return widgetService.searchByName(q);
25+
}
26+
27+
/**
28+
* Get a widget by ID
29+
* @param id the widget ID
30+
* @return the widget if found, or 404 if not found
31+
*/
32+
@GetMapping("{id}")
33+
public ResponseEntity<Widget> get(@PathVariable UUID id) {
34+
return widgetService.findById(id)
35+
.map(ResponseEntity::ok)
36+
.orElse(ResponseEntity.notFound().build());
37+
}
38+
39+
@PostMapping(
40+
value = "/insert",
41+
consumes = MediaType.APPLICATION_JSON_VALUE,
42+
produces = MediaType.APPLICATION_JSON_VALUE
43+
)
44+
public ResponseEntity<Widget> insert(@RequestBody Widget widget) {
45+
// Generate ID server-side if not provided
46+
if (widget.getId() == null) {
47+
widget.setId(UUID.randomUUID());
48+
}
49+
50+
Widget saved = widgetService.save(widget);
51+
return ResponseEntity.ok(saved);
52+
}
53+
54+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.squidmin.java.spring.maven.cloudsql.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Id;
6+
import jakarta.persistence.Table;
7+
import org.hibernate.annotations.JdbcTypeCode;
8+
import org.hibernate.type.SqlTypes;
9+
10+
import java.time.Instant;
11+
import java.util.Map;
12+
import java.util.UUID;
13+
14+
@Entity
15+
@Table(name = "widgets")
16+
public class Widget {
17+
18+
@Id
19+
@Column(name = "id", nullable = false, columnDefinition = "uuid")
20+
private UUID id;
21+
22+
@Column(name = "name", nullable = false)
23+
private String name;
24+
25+
// DB sets default now(); mark read-only so we don’t overwrite it
26+
@Column(name = "created_at", columnDefinition = "timestamptz", updatable = false, insertable = false)
27+
private Instant createdAt;
28+
29+
@JdbcTypeCode(SqlTypes.JSON)
30+
@Column(name = "meta", columnDefinition = "jsonb")
31+
private Map<String, Object> meta;
32+
33+
public Widget() {}
34+
35+
public UUID getId() { return id; }
36+
public void setId(UUID id) { this.id = id; }
37+
38+
public String getName() { return name; }
39+
public void setName(String name) { this.name = name; }
40+
41+
public Instant getCreatedAt() { return createdAt; }
42+
43+
public Map<String, Object> getMeta() { return meta; }
44+
public void setMeta(Map<String, Object> meta) { this.meta = meta; }
45+
46+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.squidmin.java.spring.maven.cloudsql.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
5+
6+
import java.util.List;
7+
import java.util.UUID;
8+
9+
public interface WidgetRepository extends JpaRepository<Widget, UUID> {
10+
List<Widget> findByNameContainingIgnoreCase(String q);
11+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.squidmin.java.spring.maven.cloudsql.service;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.squidmin.java.spring.maven.cloudsql.domain.Widget;
5+
import org.squidmin.java.spring.maven.cloudsql.repository.WidgetRepository;
6+
7+
import java.util.List;
8+
import java.util.Optional;
9+
import java.util.UUID;
10+
11+
@Service
12+
public class WidgetService {
13+
14+
private final WidgetRepository repo;
15+
16+
public WidgetService(WidgetRepository repo) {
17+
this.repo = repo;
18+
}
19+
20+
public List<Widget> findAll() {
21+
return repo.findAll();
22+
}
23+
24+
public Optional<Widget> findById(UUID id) {
25+
return repo.findById(id);
26+
}
27+
28+
public List<Widget> searchByName(String q) {
29+
return (q == null || q.isBlank()) ? repo.findAll() : repo.findByNameContainingIgnoreCase(q);
30+
}
31+
32+
public Widget save(Widget widget) {
33+
return repo.save(widget);
34+
}
35+
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
spring:
22
application:
33
name: google-cloud-sql
4+
datasource:
5+
url: jdbc:postgresql://127.0.0.1:5400/jm_demo_db
6+
username: username
7+
password: ${DB_PASSWORD:password}
8+
driver-class-name: org.postgresql.Driver
9+
hikari:
10+
maximum-pool-size: 10
11+
minimum-idle: 2
12+
13+
jpa:
14+
open-in-view: false
15+
hibernate:
16+
ddl-auto: none
17+
properties:
18+
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.squidmin.java.spring.maven.cloudsql.actuator;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.boot.test.context.SpringBootTest;
6+
import org.springframework.boot.test.web.client.TestRestTemplate;
7+
import org.springframework.boot.test.web.server.LocalServerPort;
8+
import org.springframework.http.ResponseEntity;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
@SpringBootTest(
13+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
14+
properties = {
15+
"management.endpoints.web.exposure.include=health,info",
16+
"management.endpoint.health.show-details=always",
17+
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1",
18+
"spring.datasource.driver-class-name=org.h2.Driver",
19+
"spring.jpa.hibernate.ddl-auto=none"
20+
}
21+
)
22+
public class ActuatorTest {
23+
24+
@LocalServerPort
25+
int port;
26+
27+
@Autowired
28+
TestRestTemplate rest;
29+
30+
@Test
31+
void health_isUp() {
32+
ResponseEntity<String> res =
33+
rest.getForEntity("http://localhost:" + port + "/actuator/health", String.class);
34+
assertThat(res.getStatusCode().is2xxSuccessful()).isTrue();
35+
assertThat(res.getBody()).contains("\"status\":\"UP\"");
36+
}
37+
38+
@Test
39+
void info_isReachable() {
40+
ResponseEntity<String> res =
41+
rest.getForEntity("http://localhost:" + port + "/actuator/info", String.class);
42+
assertThat(res.getStatusCode().is2xxSuccessful()).isTrue();
43+
}
44+
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
spring:
22
application:
33
name: google-cloud-sql
4+
datasource:
5+
url: jdbc:postgresql://127.0.0.1:5400/jm_demo_db
6+
username: username
7+
password: ${DB_PASSWORD:password}
8+
driver-class-name: org.postgresql.Driver
9+
hikari:
10+
maximum-pool-size: 10
11+
minimum-idle: 2
12+
13+
jpa:
14+
open-in-view: false
15+
hibernate:
16+
ddl-auto: none
17+
properties:
18+
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect

0 commit comments

Comments
 (0)