{"id":15804940,"url":"https://github.com/ddobrin/inner-loop-developer-workshop","last_synced_at":"2025-03-31T22:02:13.780Z","repository":{"id":134175682,"uuid":"474037648","full_name":"ddobrin/inner-loop-developer-workshop","owner":"ddobrin","description":"Demo of Inner Loop Development with Google Cloud","archived":false,"fork":false,"pushed_at":"2022-04-05T14:45:28.000Z","size":45,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-06T02:02:52.398Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ddobrin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-25T14:16:43.000Z","updated_at":"2022-03-25T15:10:48.000Z","dependencies_parsed_at":null,"dependency_job_id":"7e4937aa-5175-4f36-8289-43e3ca880ea3","html_url":"https://github.com/ddobrin/inner-loop-developer-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ddobrin%2Finner-loop-developer-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ddobrin%2Finner-loop-developer-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ddobrin%2Finner-loop-developer-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ddobrin%2Finner-loop-developer-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ddobrin","download_url":"https://codeload.github.com/ddobrin/inner-loop-developer-workshop/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246547387,"owners_count":20794970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-05T02:02:20.609Z","updated_at":"2025-03-31T22:02:13.751Z","avatar_url":"https://github.com/ddobrin.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Inner Loop App Dev with Spring Boot and Testcontainers on Google Cloud\n\nThe workshop dives into the Inner Loop App Dev process of a Spring Boot Java application in Google Cloud.\n\n## Key Points \nIn this workshop you will learn how to:\n* Generate and configure build and deployment manifests for K8s using Skaffold and Jib \n* Build a simple CRUD REST service with a local Postgres backend\n* Add unit tests for the app, leveraging Testcontainers\n* Run/Debug the app in a GKE cluster\n* Observe an error in the app, set breakpoints, debug and fix the app in a GKE cluster\n* Observe hot redeploy in action for app fixes\n* Validate that unit tests didn't cover the problem in the app\n* Add the missing unit test - lesson learned !\n\nAdvanced Section - optional:\n* Connect the app to a Google CloudSQL instance (CloudSQL for Postgres)\n* Run and test the app with managed Google Cloud services\n\n## Prequisites\nIDE\n* A cloud editor with Cloud Code, ex: Cloud Shell Editor or \n* VS Code or IntelliJ with Cloud Code installed\n\nGoogle Cloud Services:\n* Enable managed Google Cloud Services - [Setup Instructions](docs/EnableServices.md)\n\nCluster\n* GKE cluster configured to be used by Cloud Code - [Setup Instructions](./docs/GKEClusterSetup.md)\n* Local K8s cluster such as minikube\n\nArtifact Registry repository\n* Create a repository in Artifact Registry - [Setup Instructions](docs/ArtifactRegistrySetup.md)\n\n## Before you begin\nFor this workshop, you need a Google Cloud [project](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects). You can create a new one, or select a project you already created:\n\n1. Select or create a Google Cloud project.\n\n    [GO TO THE PROJECT SELECTOR PAGE](https://console.corp.google.com/projectselector2/home/dashboard)\n\n2. Enable billing for your project.\n\n    [ENABLE BILLING](https://support.google.com/cloud/answer/6293499#enable-billing)\n\n3. Create environment variables to use throughout this workshop:\n\n```shell\nexport PROJECT_ID=$(gcloud config get-value project)\nexport PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')\n```\n\n## Start the workshop from a starter Spring Boot application\n\nClone the starter app code from Github\n```shell\ngit clone https://github.com/ddobrin/inner-loop-developer-workshop.git\n\n# switch to the starter app folder\ncd spring-boot-starter-app\n```\n\n## Explore the starter app codebase\n\n* On your local machine, open a `VS Code workspace` for example, by executing `code .`\n* In Cloudshell, open the Cloudshell Editor; then, in the `Explorer` view, navigate to the `spring-boot-starter-app` folder, right-click and select `cloudshell workspace .`\n\nNotes - source code:\n* Source code is provisioned in the `src` folder and the application starts in the DemoApplication Java class\n* A starter Rest controller has been created for the `/` endpoint in the `src/main/java/com/example/HelloController` Java class. The controllwer displays a simple greeting, indicating the \u003cenvironment\u003e where it is running\n* This start Spring Boot app has `no container specific code` \n* The code can be built using using `mvn` or `gradle`. In this specific example, Maven will be used. Locate `pom.xml` file in the project root and observe that it is configured to use Java 11, Boot 2.6.x and Spring Cloud 2021.x\n\n## Generate, configure build and deployment manifests for K8s using Skaffold and Jib\n\nOpen the `pom.xml` and enable the Spring Boot DevTools, a development setting which supports Hot Deploy in future stages. A `dev profile` is being used, as this setting will not be used in a Production environment.\n```xml\n  \u003c!--  Spring profiles--\u003e\n  \u003cprofiles\u003e\n      \u003cprofile\u003e\n      \u003cid\u003esync\u003c/id\u003e\n      \u003cdependencies\u003e\n          \u003cdependency\u003e\n          \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n          \u003cartifactId\u003espring-boot-devtools\u003c/artifactId\u003e\n          \u003c/dependency\u003e\n      \u003c/dependencies\u003e\n      \u003c/profile\u003e\n  \u003c/profiles\u003e\n```\n\nAdd the Jib plugin in the `\u003cbuild/plugins\u003e` section, to enable building with Jib\n```xml\n  \u003cbuild\u003e\n    \u003cplugins\u003e\n      \u003cplugin\u003e\n        \u003cgroupId\u003ecom.google.cloud.tools\u003c/groupId\u003e\n        \u003cartifactId\u003ejib-maven-plugin\u003c/artifactId\u003e\n        \u003cversion\u003e3.2.0\u003c/version\u003e\n      \u003c/plugin\u003e  \n    \u003c/plugins\u003e\n  \u003c/build\u003e\n```\n\nIn your IDE of choice, open a Terminal window. The next steps involve:\n* initializing Skafold\n* generating K8s deployment manifests\n* setting Jib up to build images when invoked from Skaffold\n\n```shell\nskaffold init --generate-manifests --XXenableJibInit\n```\n* using the arrow keys, select the `Jib Maven Plugin (com.example:demo-app, pom.xm*l)` option\n* type in port `8080` to port-forward to\n* type `y` when prompted `Do you want to write this configuration, along with the generated k8s manifests, to skaffold.yaml?`\n\nObserve the output, as two files are generated in your workspace: `skaffold.yaml` and `deployment.yaml`\n```\n    Generated manifest deployment.yaml was written\n    Configuration skaffold.yaml was written\n    You can now run [skaffold build] to build the artifacts\n    or [skaffold run] to build and deploy\n    or [skaffold dev] to enter development mode, with auto-redeploy\n```\n\nOpen the newly generated `skaffold.yaml` \n* Select the image name currently sent as `pom-xml-image`. \n* Right click and choose `Change All Occurences`  \n* Type in the new name as `demo-app`\n\nOpen now the `deployment.yaml` file and change all occurences of `pom-xml-image` to `demo-app`. The file contains 2 manifests\n* a K8s deployment that deploys a pod with the container image \n* a K8s service that exposes the deployment\n\n## Enable Hot Redeploy\nTo support the hot redeploy in Skaffold, when building with Jib, update the `skaffold.yaml` file in the `build` section, by adding configuration to skip unit tests when deploying to k8s, enabling sync and adding the `dev profile` to the build. The file should look as\n```yaml\nbuild:\n  artifacts:\n  - image: demo-app\n    jib:\n      project: com.example:demo-app\n      args:\n      - -Dmaven.test.skip=true\n      - -Psync\n    sync:\n      auto: true\n```\n\n## Validate your configuration by running a quick Build \u0026 Deploy directly against the GKE cluster\nThis will be the base of the app going forward.\n\nIn your IDE, click F1 and execute `Cloud Code: Run on Kubernetes` or click the Cloud Code extension link and select the same option\n* Choose context - point to the GKE cluster called `lab-cluster`\n* Choose Artifact Registry - select Browse Artifact Registry - select the `demo-app` folder\n* Within the Artifact Registry demo-app folder, use the default `.` as the image location\n\nSkaffold will start to build the app and deploy it to K8s. In the `Output` tab, select the `Cloud Code - Kubernetes` view, observe the `Status`, Build Containers`, `Deploy to Cluster` and `Portforward URLs` steps.\n\nClick on `Portforwarded URLS` as the deployment is successul and observe the output:\n```\nPort forwarding service/demo-app in namespace default, remote port 8080 -\u003e http://127.0.0.1:8080\n```\n\nClick on the link and observe the output:\n```\nHello from your local environment!\n```\n\nYou can also send a cUrl / HTTPie request for testing:\n```shell\ncurl 127.0.0.1:8080\n   or \nhttp :8080\n```\n\nYou have now a running web application deployed and running in your GKE cluster and are good to go for building out the CRUD application for the workshop !!!\n\nStop the session using `SHIFT + F5` or select `Stop Debugging` from the menu or click the `Stop` button the Cloud Code - Kubernetes view.\nSelect the option to `Clean up after each run` to remove deployed artifacts.\n\n## Build a simple CRUD REST service with a local Postgres backend\n\nTo build a CRUD service within the starter app, the following areas need to be addressed:\n* develop the CRUD service code\n* add configuration for the backend database accessed by the service\n* update the dependencies in the Maven POM file\n* add containerized unit and integration tests for the backend leveraging Testcontainers\n\n# Let's start writing some code ...\n\n## Add the CRUD Service code \nWe'll develop a `Quote` service, which would allow us to work with quotes collected from famous people throughout history.\n\nThe code for the Quote service will be developed in the `com.example` package.\n\nStart by creating an file for the Entity class:  Quote.java\n```java\npackage com.example;\n\nimport javax.persistence.Column;\nimport javax.persistence.Entity;\nimport javax.persistence.Id;\nimport javax.persistence.Table;\nimport java.util.Objects;\n\n@Entity\n@Table(name = \"quotes\")\npublic class Quote\n{\n    @Id\n    @Column(name = \"id\")\n    private Integer id;\n\n    @Column(name=\"quote\")\n    private String quote;\n\n    @Column(name=\"author\")\n    private String author;\n\n    public Integer getId() {\n        return id;\n    }\n\n    public void setId(Integer id) {\n        this.id = id;\n    }\n\n    public String getQuote() {\n        return quote;\n    }\n\n    public void setQuote(String quote) {\n        this.quote = quote;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = author;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n      if (this == o) {\n        return true;\n      }\n      if (o == null || getClass() != o.getClass()) {\n        return false;\n      }\n        Quote quote1 = (Quote) o;\n        return Objects.equals(id, quote1.id) \u0026\u0026\n                Objects.equals(quote, quote1.quote) \u0026\u0026\n                Objects.equals(author, quote1.author);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(id, quote, author);\n    }\n}\n```\n\nThe intent is to use JPA for persisting the data, therefore the need to create a repository class `QuoteRepository`, which extends the Spring `JPARepository` interface and allows the creation of custom code. This class will create a `findRandomQuote` custom method.\n\nCreate the `QuoteRepository.java` file in the same package.QuoteRepository.java\n```java\npackage com.example;\n\nimport org.springframework.data.jpa.repository.JpaRepository;\nimport org.springframework.data.jpa.repository.Query;\n\npublic interface QuoteRepository extends JpaRepository\u003cQuote,Integer\u003e {\n\n    @Query( nativeQuery = true, value =\n            \"SELECT id,quote,author FROM quotes ORDER BY RANDOM() LIMIT 1\")\n    Quote findRandomQuote();\n}\n```\n\nTo expose the endpoint for the service, a `QuoteController` class will provide the functionality.\n\nCreate the `QuoteController.java` file in the same package:\n```java\npackage com.example;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\nimport org.springframework.dao.EmptyResultDataAccessException;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.DeleteMapping;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.PutMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class QuoteController {\n\n    private final QuoteRepository quoteRepository;\n\n    public QuoteController(QuoteRepository quoteRepository) {\n        this.quoteRepository = quoteRepository;\n    }\n\n    @GetMapping(\"/random-quote\") \n    public Quote randomQuote()\n    {\n        return quoteRepository.findRandomQuote();  \n    }\n\n    @GetMapping(\"/quotes\") \n    public ResponseEntity\u003cList\u003cQuote\u003e\u003e allQuotes()\n    {\n        try {\n            List\u003cQuote\u003e quotes = new ArrayList\u003cQuote\u003e();\n            \n            quoteRepository.findAll().forEach(quotes::add);\n\n            if (quotes.size()==0 || quotes.isEmpty()) \n                return new ResponseEntity\u003cList\u003cQuote\u003e\u003e(HttpStatus.NO_CONTENT);\n                \n            return new ResponseEntity\u003cList\u003cQuote\u003e\u003e(quotes, HttpStatus.OK);\n        } catch (Exception e) {\n            System.out.println(e.getMessage());\n            return new ResponseEntity\u003cList\u003cQuote\u003e\u003e(HttpStatus.INTERNAL_SERVER_ERROR);\n        }        \n    }\n\n    @PostMapping(\"/quotes\")\n    public ResponseEntity\u003cQuote\u003e createQuote(@RequestBody Quote quote) {\n        try {\n            Quote saved = quoteRepository.save(quote);\n            return new ResponseEntity\u003cQuote\u003e(saved, HttpStatus.CREATED);\n        } catch (Exception e) {\n            System.out.println(e.getMessage());\n            return new ResponseEntity\u003cQuote\u003e(HttpStatus.INTERNAL_SERVER_ERROR);\n        }\n    }     \n\n    @PutMapping(\"/quotes/{id}\")\n    public ResponseEntity\u003cQuote\u003e updateQuote(@PathVariable(\"id\") Integer id, @RequestBody Quote quote) {\n        try {\n            Optional\u003cQuote\u003e existingQuote = quoteRepository.findById(id);\n            \n            if(existingQuote.isPresent()){\n                Quote updatedQuote = existingQuote.get();\n                updatedQuote.setAuthor(quote.getAuthor());\n                updatedQuote.setQuote(quote.getQuote());\n\n                return new ResponseEntity\u003cQuote\u003e(updatedQuote, HttpStatus.OK);\n            } else {\n                return new ResponseEntity\u003cQuote\u003e(HttpStatus.NOT_FOUND);\n            }\n        } catch (Exception e) {\n            System.out.println(e.getMessage());\n            return new ResponseEntity\u003cQuote\u003e(HttpStatus.INTERNAL_SERVER_ERROR);\n        }\n    }     \n\n    @DeleteMapping(\"/quotes/{id}\")\n    public ResponseEntity\u003cHttpStatus\u003e deleteQuote(@PathVariable(\"id\") Integer id) {\n        try {\n            quoteRepository.deleteById(id);\n            return new ResponseEntity\u003c\u003e(HttpStatus.NO_CONTENT);\n        } catch (RuntimeException e) {\n            System.out.println(e.getMessage());\n            return new ResponseEntity\u003c\u003e(HttpStatus.INTERNAL_SERVER_ERROR);\n        }\n    }    \n}\n```\n\nIt exposes the following endpoints:\n```java\n    // retrieve a random quote\n    @GetMapping(\"/random-quote\") \n\n    // retrieve all quotes from the backend\n    @GetMapping(\"/quotes\") \n\n    // create a new quote\n    @PostMapping(\"/quotes\")\n\n    // update and existing quote\n    @PutMapping(\"/quotes/{id}\")\n\n    // delete a quote\n    @DeleteMapping(\"/quotes/{id}\")\n```\n\n## Add configuration for the backend database accessed by the service\n\nOpen the `application.yaml file under `src/main/resources` and add a parameterized Spring configuration for the backend.\n```yaml\nspring:\n  config:\n    activate:\n      on-profile: cloud-dev\n  datasource:\n    url: 'jdbc:postgresql://${DB_HOST:127.0.0.1}/${DB_DATABASE:quote_db}'\n    username: '${DB_USER:user}'\n    password: '${DB_PASS:password}'\n  jpa:\n    properties:\n      hibernate:\n        jdbc:\n          lob:\n            non_contextual_creation: true\n        dialect: org.hibernate.dialect.PostgreSQLDialect\n    hibernate:\n      ddl-auto: update\n```\n\nThe DB_HOST, DB_DATABASE, DB_USER and DB_PASS parameters will be set via externalized K8s configuration.\n\n## Update the dependencies in the Maven POM file\nBefore the app can be compiled, the Maven POM file must be updated with the Spring JPA, Postgres, Flyway and H2 dependencies.\n\n```xml\n    \u003c!--  Database dependencies--\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n      \u003cartifactId\u003espring-boot-starter-data-jpa\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003ecom.h2database\u003c/groupId\u003e\n      \u003cartifactId\u003eh2\u003c/artifactId\u003e\n      \u003cscope\u003eruntime\u003c/scope\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.postgresql\u003c/groupId\u003e\n      \u003cartifactId\u003epostgresql\u003c/artifactId\u003e\n      \u003cscope\u003eruntime\u003c/scope\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.flywaydb\u003c/groupId\u003e\n      \u003cartifactId\u003eflyway-core\u003c/artifactId\u003e\n    \u003c/dependency\u003e\n```\n\n## Add containerized unit tests using Testcontainers\n\nLet's start using Testcontainers in order to test the Quote service in a containerized manner, similar to service execution in Production.\n\nTo this end, we have to start by creating a good starting point for the test strategy, in two steps:\n* set up data\n* write up tests\n\nUsing the Java API, developers can easily provision databases at application start-up, portable across backends (local or cloud) using Flyway.\nFlyway is an open-source database migration tool, which strongly favors simplicity and convention over configuration.\n\nLet's provision a simple set of 5 quotes in the database, executed at app start-up.\n\n* create a folder `db/migration` under `src/main/resources`: `src/main/resources/db/migration/`. \n* create a SQL file: `V1__create_quotes_table.sql`\n* paste the following SQL lines\n```sql\nCREATE TABLE quotes(\n   id INTEGER PRIMARY KEY,\n   quote VARCHAR(1024),\n   author VARCHAR(256)\n);\n\nINSERT INTO quotes (id,quote,author) VALUES (1,'Never, never, never give up','Winston Churchill');\nINSERT INTO quotes (id,quote,author) VALUES (2,'While there''s life, there''s hope','Marcus Tullius Cicero');\nINSERT INTO quotes (id,quote,author) VALUES (3,'Failure is success in progress','Anonymous');\nINSERT INTO quotes (id,quote,author) VALUES (4,'Success demands singleness of purpose','Vincent Lombardi');\nINSERT INTO quotes (id,quote,author) VALUES (5,'The shortest answer is doing','Lord Herbert');\n```\n\nIn the pom.xml, let's add the required dependencies.\n\nAdd the testcontainers version under the `\u003cproperties\u003e` section:\n```xml\n\u003ctestcontainers.version\u003e1.16.3\u003c/testcontainers.version\u003e\n\n# Section will look like\n  \u003cproperties\u003e\n    \u003cjava.version\u003e1\u003c/java.version\u003e\n\t\u003cspring-cloud.version\u003e2021.0.1\u003c/spring-cloud.version\u003e\n    \u003ctestcontainers.version\u003e1.16.3\u003c/testcontainers.version\u003e\n  \u003c/properties\u003e\n```\n\nAdd the Junit and Testcontainers dependencies in the `\u003cdependencies\u003e` section:\n```xml\n    \u003c!-- Test dependencies --\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n      \u003cartifactId\u003espring-boot-starter-test\u003c/artifactId\u003e\n      \u003cscope\u003etest\u003c/scope\u003e\n      \u003cexclusions\u003e\n        \u003cexclusion\u003e\n          \u003cgroupId\u003eorg.junit.vintage\u003c/groupId\u003e\n          \u003cartifactId\u003ejunit-vintage-engine\u003c/artifactId\u003e\n        \u003c/exclusion\u003e\n      \u003c/exclusions\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.testcontainers\u003c/groupId\u003e\n      \u003cartifactId\u003ejunit-jupiter\u003c/artifactId\u003e\n      \u003cscope\u003etest\u003c/scope\u003e\n    \u003c/dependency\u003e\n    \u003cdependency\u003e\n      \u003cgroupId\u003eorg.testcontainers\u003c/groupId\u003e\n      \u003cartifactId\u003epostgresql\u003c/artifactId\u003e\n      \u003cscope\u003etest\u003c/scope\u003e\n    \u003c/dependency\u003e\n```\n\nAdd the Testcontainers BOM in the `\u003cdependencyManagement/dependencies\u003e` section:\n```xml\n          \u003cdependency\u003e\n            \u003cgroupId\u003eorg.testcontainers\u003c/groupId\u003e\n            \u003cartifactId\u003etestcontainers-bom\u003c/artifactId\u003e\n            \u003cversion\u003e${testcontainers.version}\u003c/version\u003e\n            \u003ctype\u003epom\u003c/type\u003e\n            \u003cscope\u003eimport\u003c/scope\u003e\n          \u003c/dependency\u003e\n```\n\nIn the source code, there are two areas to be addressed: test code and test configuration.\n\nAdd the following configuration to the `application-test.yaml` file under `src/test/resources`:\n```yaml\nspring:\n  datasource:\n    url: \"jdbc:tc:postgresql:13:///quotes?TC_TMPFS=/testtmpfs:rw\"\n```\n\nAdd the test code in the `src/test/com/example` folder as `QuotesRepositoryTest.java`:\n```java\npackage com.example;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.testcontainers.DockerClientFactory.TESTCONTAINERS_LABEL;\n\nimport com.github.dockerjava.api.model.Container;\nimport java.util.Map;\n\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.ActiveProfiles;\nimport org.testcontainers.DockerClientFactory;\n\n@SpringBootTest\n@ActiveProfiles(\"test\")\nclass QuotesRepositoryTest {\n\n  @BeforeEach\n  void debug() {\n    // print a list of all the containers test containers are currently running\n    var client = DockerClientFactory.instance().client();\n    var containers = client.listContainersCmd()\n        .withLabelFilter(Map.of(TESTCONTAINERS_LABEL, \"true\")).exec();\n    for (Container container : containers) {\n      System.out.println(container.getImage());\n    }\n  }\n\n  @Test\n  @DisplayName(\"A random quote is returned\")\n  void testRandomQuotes(@Autowired QuoteRepository quoteRepository) {\n    var quote = quoteRepository.findRandomQuote();\n    assertThat(quote).isNotNull();\n  }\n\n  @Test\n  @DisplayName(\"All quotes are returned\")\n  void testAllQuotes(@Autowired QuoteRepository quoteRepository) {\n    var quotes = quoteRepository.findAll();\n    assertThat(quotes).isNotNull();\n  }\n\n  @Test\n  @DisplayName(\"Create a quote\")\n  void testCreateQuote(@Autowired QuoteRepository quoteRepository){\n    var quote = new Quote();\n    quote.setId(6);\n    quote.setAuthor(\"Confucius\");\n    quote.setQuote(\"Our greatest glory is not in never falling, but in rising every time we fall\");\n\n    var result = quoteRepository.save(quote);\n    assertThat(result.getAuthor()).isEqualTo(\"Confucius\");\n  }\n\n  @Test\n  @DisplayName(\"Delete a quote - good\")\n  void testDeleteQuoteGood(@Autowired QuoteRepository quoteRepository){\n    var quote = new Quote();\n    quote.setId(6);\n    quote.setAuthor(\"Confucius\");\n    quote.setQuote(\"Our greatest glory is not in never falling, but in rising every time we fall\");\n\n    var result = quoteRepository.save(quote);\n    assertThat(result.getAuthor()).isEqualTo(\"Confucius\");\n\n    assertDoesNotThrow(() -\u003e {\n      quoteRepository.deleteById(6);\n    });\n  }\n\n}\n```\n\nIn order to validate a correct build of the application, let's proceed to deploy the app to the GKE cluster.\nAs part of the build process, the app will be built, unit tests executed and any failure will be reported.\n\nRepeat the process to deploy to GKE using Cloud Code, for example F1 + select `Cloud Code: Run on Kubernetes` !\n\n## Test the CRUD service in GKE\n\nLet's use either cURL / HTTPie for testing the app, from a Terminal window:\n```shell\n# run repeatedly a GET against the random-quote endpoint\n# observe repeated call returning different quotes\ncurl -v 127.0.0.1:8080/random-quote\n  or\nhttp :8080/random-quote\n\n# create a new quote, with id=6\n# observe the request being echo'ed back\ncurl -v -H 'Content-Type: application/json' -d '{\"id\":\"6\",\"author\":\"Henry David Thoreau\",\"quote\":\"Go confidently in the direction of your dreams! Live the life you’ve imagined\"}' -X POST 127.0.0.1:8080/quotes\n  or \nhttp PUT :8080/quotes/6 author=\"Henry David Thoreau\" quote=\"Go confidently in the direction of your dreams! Live the life you’ve imagined\" id=\"6\"\n\n# delete a quote\ncurl -v -X DELETE 127.0.0.1:8080/quotes/6\n  or \nhttp DELETE :8080/quotes/6\n```\n\n## Execution failure encountered\nRun the last request again, after the quote has previously been deleted and observe an `HTTP:500 Internal Server Error`. \n\nLet's find out why this is happening !!!\n\n1. Stop the Cloud Code session and restart as a Debug session:\n* F1 + Cloud Code: Debug on Kubernetes\n* Click Cloud Code link in the IDE and select the Cloud Code: Debug on Kubernetes option\n\n2. The error occurred in the DELETE operation, therefore let's open the `QuoteController` class, go to the `deleteQuote()` method and set a breakpoint on the line where er delete an item from the database: `quoteRepository.deleteById(id);`\n\n3. Run the `delete` command again and observe the debug line stopped in the QuoteController class.\nIn the debugger, `step over` the `deleteById()` invocation and observe that an exception is thrown, due to the fact that the `quote` with `id=6` does not exist in the database.\n\n4. Note that in the code we caught a very generic `RuntimException`, which sends back an Internal Server Error HTTP 500.\n\n5. The code is incorrect and the exception block should be refactored to catch the `EmptyResultDataAccessException` exception and send back an HTTP 404 not found status code.\n\nLet's correct the error. With the Debug session still running `!!!`, add the following block to the code:\n```java \n    ...\n        } catch(EmptyResultDataAccessException e){\n            return new ResponseEntity\u003cHttpStatus\u003e(HttpStatus.NOT_FOUND);\n        }\n    ...\n    // method should look like\n    public ResponseEntity\u003cHttpStatus\u003e deleteQuote(@PathVariable(\"id\") Integer id) {\n        try {\n            quoteRepository.deleteById(id);\n            return new ResponseEntity\u003c\u003e(HttpStatus.NO_CONTENT);\n        } catch(EmptyResultDataAccessException e){\n            return new ResponseEntity\u003cHttpStatus\u003e(HttpStatus.NOT_FOUND);\n        } catch (RuntimeException e) {\n            System.out.println(e.getMessage());\n            return new ResponseEntity\u003c\u003e(HttpStatus.INTERNAL_SERVER_ERROR);\n        }\n    } \n```\n \nStep through the debugger and observe the `EmptyResultDataAccessException` being caught and an HTTP 404 Not Found returned to the caller.\nThe `Local Variables` can be observed in the `Cloud Code - Kubernetes` view!\n\nStop the debugging session.\n\nIn this section you have learned how to debug directly in a Kubernetes cluster in GKE, set breakpoints, fuind and fix errors.\n\n## Last step - add the missing unit test\nA lesson was learned: pay attention to write good tests before deploying !\n\nLet's correct this by adding a test method to the `QuotesRepositoryTest` test class:\n```java\n  @Test\n  @DisplayName(\"Delete a quote - failed\")\n  void testDeleteQuote(@Autowired QuoteRepository quoteRepository){\n    assertThatThrownBy(() -\u003e {\n      quoteRepository.deleteById(100);\n    }).isInstanceOf(org.springframework.dao.EmptyResultDataAccessException.class);\n  }\n  ```\n\nTo validate that the test method is correct, we can simply run a `mvn verify` command from a terminal window and observe all our tests passing.\n\nAlternatively, you can open `Test` view (test glass icon), right click on QuotesRepositoryTest and `Run Tests`. You should observe 5 tests executing correctly.\n\n\n### This concludes the `mandatory` part of the workshop - please run the Optional part of the workshop, where the app will be connected to a CloudSQL for Postgres managed instance\n\n------\n\n# Optional: Connect the app to a Google CloudSQL instance (CloudSQL for Postgres)\n\nThe starter app has evolved throughout the course of the workshop, with the addition of a the `Quote` CRUD service, connected to a Postgres backend.\n\n`Quick reminders` \n* up to this point, the backend has been implemented by the H2 Java Database, running in-memory. Testing has been performed using a containerized Postgres database, abstracted through the usage of Testcontainers.\n* at application start, you could observe, when selecting the `Kubernetes: Run/Debug Detailed` option in the dropdown located in the `Output` tab of the `Cloud Code - Kubernetes` view, the database being used, an in-memory database:\n  ```yaml\n  [demo-app]... --- [  restartedMain] o.f.c.i.database.base.BaseDatabaseType   : Database: jdbc:h2:mem:faa65c63-110b-4723-95d8-bbd3fb2642cb (H2 1.4)\n  ```\n* the datasource configuration has already been configured using externalized variables in the `src/main/resources/application.yaml` file. This configuration is being activated by the `cloud-dev` profile set in the deployment manifest `deployment.yaml`:\n  ```yaml\n  spring:\n    config:\n      activate:\n        on-profile: cloud-dev\n    datasource:\n      url: 'jdbc:postgresql://${DB_HOST:127.0.0.1}/${DB_DATABASE:quote_db}'\n      username: '${DB_USER:user}'\n      password: '${DB_PASS:password}'\n  ...    \n  ```    \n\nAt this time, the app can be enhanced and connected directly to a `CloudSQL for Postgres managed instance in the Google Cloud`.\n\nTo set up a CloudSQL for Postgres database instance, please follow the instructions in [CloudSQL Setup Instructions](./docs/CloudSQLSetup.md).\n\nThe following additions to the `deployment.yaml` file allow the application to connect to the CloudSQL instances.\nNotes:\n* TARGET - configures the variable to indicate the environment where the app is executed\n* SPRING_PROFILES_ACTIVE - shows the active Spring profile, which will be configured to `cloud-dev`\n* DB_HOST - the private IP for the database, which has been noted when the database instance has been created or by clicking `SQL` in the Navigation Menu of the Google Cloud Console - please change the value !\n* DB_USER and DB_PASS - as set in the CloudSQL instance configuration, stored as a Secret in GCP\n\n```yaml\n        env:\n          - name: PORT\n            value: \"8080\"\n          - name: TARGET\n            value: \"Local Dev - CloudSQL Database - K8s Cluster\"\n          # set the profile to use\n          - name: SPRING_PROFILES_ACTIVE\n            value: cloud-dev\n          - name: DB_HOST\n            value: PRIVATE-IP-OF-DATABASE  # example \"172.24.0.3\" \n          - name: DB_PORT\n            value: \"5432\"  \n          - name: DB_USER\n            valueFrom:\n              secretKeyRef:\n                name: gke-cloud-sql-secrets\n                key: username\n          - name: DB_PASS\n            valueFrom:\n              secretKeyRef:\n                name: gke-cloud-sql-secrets\n                key: password\n          - name: DB_NAME\n            valueFrom:\n              secretKeyRef:\n                name: gke-cloud-sql-secrets\n                key: database       \n```                \n\nSave the file and start the app in the GKE cluster from `Cloud Code: Run on Kubernetees\n\nObserve, in the `Kubernetes: Run/Debug - Detailed` dropdown that the app connects to the previously configured CloudSQL instance at \u003cprivate IP of database\u003e\n```\n...\n[demo-app]... --- [  restartedMain] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:postgresql://172.24.0.3/quote_db'\n...\n```\n\nTest the app and observe that the root endpoint indicates in its output the environment change: `Local Dev - CloudSQL Database - K8s Cluster environment!`\n```\nLet's use either cURL or HTTPie for testing the app, from a Terminal window:\n```shell\ncurl -v 127.0.0.1:8080\n  or\nhttp :8080\n\n# Output: Hello from your Local Dev - CloudSQL Database - K8s Cluster environment!\n\ncurl -v 127.0.0.1:8080/random-quote\n  or\nhttp :8080/random-quote\n\n# Output: \n{\n    \"author\": \"Marcus Tullius Cicero\",\n    \"id\": 2,\n    \"quote\": \"While there's life, there's hope\"\n}\n```\n\nStop the application running in GKE!\n\n## Congratulations - you have successfully completed the workshop !","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fddobrin%2Finner-loop-developer-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fddobrin%2Finner-loop-developer-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fddobrin%2Finner-loop-developer-workshop/lists"}