Google Cloud Platform

How to use the toolkit’s GCP containers and services.

Key takeaways

CloudFunctionService Abstract service that receives remote calls via RPC or REST protocols in the Google Function.
CommandableCloudFunctionService Abstract service that receives remote calls via RPC or REST protocols in the Google Function and connects them to automatically generated actions for commands defined in ICommandable components.
CloudFunction Abstract Google Function that acts as a container. Instantiates and runs components, and exposes them via an external entry point.
CommandableCloudFunction Abstract Google Function that acts as a container. Instantiates and runs components, and exposes them via an external entry point. This container is a subclass of the CloudFunction class with added functionality that automatically generates actions for commands defined in ICommandable components.

Introduction

In this tutorial, you will learn how to create a microservice and package it in a Google Cloud Platform (GCP) container. The content is divided into five sections:

  1. GCP containers and services: A basic explanation of the different containers and services available in the GCP module.
  2. Basic microservices: An explanation of how to build basic microservices using GCP containers and services.
  3. Combining basic microservices: An explanation of how to further expand the basic microservice examples, providing solutions for complex business scenarios.
  4. Testing: An explanation of how to test GCP-based microservices.
  5. Wrapping up: A summary of the learned concepts.

Upon completion of this tutorial, you will gain an understanding of the various possibilities that the containers and services available in the GCP module offer for microservice design.

GCP containers and services

The GCP module includes containers and services that conform to Google Cloud’s REST protocol. They are:

Containers

Containers are used to run microservices within a specific environment. They contain all the files, libraries and configuration files necessary for the microservice’s execution.

One such environment is Google Cloud Functions, which is a serverless execution environment for building and connecting cloud services.

The Pip.Services toolkit offers two types of containers for this environment, namely, CloudFunction and CommandableCloudFunction. The second is a subclass of the first and provides additional functionality that automatically generates actions for commands defined in ICommandable components. The following diagram shows the relations between these components:

figure 1

Services

Additionally, the toolkit offers two types of services: the CloudFunctionService and the CommandableCloudFunctionService. Both are used to connect microservices to external resources. The second service is a subclass of the first and adds additional functionality that automatically generates actions for commands defined in ICommandable components. The following diagram shows their relation:

figure 2

Basic microservices

A basic GCP microservice contains a controller, which can expose commands directly or be made to include a service layer. Additionally, actions can be registered in the container/service itself or, alternatively, a CommandSet component can be added to the microservice. This part of the tutorial explains these four basic cases.

Microservice doesn’t require a service layer

The following sections explain how to create microservices that expose commands directly, without using a service layer, and how to package them into a container. For this, the toolkit offers two options: either registering the actions in the container, or using a CommandSet component. In the examples below, actions are represented by the greetings() method, which, given a name, returns “Hello, <name>!”.

Registering actions in the container

In this case, the microservice basically consists of a controller packaged in a container. The microservice has the following characteristics:

  1. It registers the actions in the container via the register() method.
  2. The business logic of the actions is defined in the controller.
  3. The container uses a factory to create the controller.

There are two ways to create an instance of the controller. The first is by adding its descriptor to the configuration file so that the container can build it automatically via the factory:

figure 3

And the second is by registering the controller as a dependency for the container:

figure 4

When using a configuration file, the container looks like this:

// service's descriptor is defined in the configuration file
export class MyCloudFunction extends CloudFunction {

    private _controller: MyController;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml"
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
    }

    private async action(req: any, res: any): Promise<void> {
        let name = req.body.name ?? 'default name';
        let result = await this._controller.greetings(name);
        
        HttpResponseSender.sendResult(req, res, result);
    }

    protected register() {
        this.registerAction('greetings', null, this.action);
    }
}




Not available

Not available

And the configuration file includes the controller’s descriptor:

# Service 
- descriptor: "mygroup:service:default:service:1.0"

When adding the controller as a dependency, the container looks like this:

// service is added as a dependency
export class MyCloudFunction extends CloudFunction {

    private _controller: MyController;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml"
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '*'))
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = this._dependencyResolver.getOneRequired('service');
    }

    private async action(req: any, res: any): Promise<void> {
        let name = req.body.name ?? 'default name';
        let result = await this._controller.greetings(name);

        HttpResponseSender.sendResult(req, res, result);
    }

    protected register() {
        this.registerAction('greetings', null, this.action);
    }
}




Not available

Not available

In both cases, implementation of the logic required from the actions will be stored in the controller:

export class MyService implements IReferenceable {
    public setReferences(references: IReferences): void {
        
    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}




Not available

Not available

Which is then registered in the factory:

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service", "1.0"), MyService)
    }
}




Not available

Not available

Finally, after grouping everything together, the resulting code is as follows:

import { CloudFunction } from "pip-services4-gcp-node";
import { Descriptor, Factory, IReferenceable, IReferences } from "pip-services4-components-node";
import { HttpResponseSender } from "pip-services4-http-node";

export class MyService implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "controller", "default", "controller", "1.0"), MyService)
    }
}

export class MyCloudFunction extends CloudFunction {

    private _controller: MyService;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml"
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('controller', new Descriptor('mygroup', 'controller', 'default', 'controller', '*'))
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('controller', new Descriptor('mygroup', 'controller', 'default', 'controller', '*'));
        // Comment out the next line of code when using the dependency resolver, uncomment for configuration file
        this._controller = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
    }

    private async action(req: any, res: any): Promise<void> {
        let name = req.body.name ?? 'default name';
        let result = await this._controller.greetings(name);
        
        HttpResponseSender.sendResult(req, res, result);
    }

    protected register() {
        this.registerAction('greetings', null, this.action);
    }
}


Not available

Not available

Using a CommandSet component

Similar to the previous case, the microservice will still contain just a controller packaged in a container, but will use a CommandSet component to define the actions instead. The main characteristics of this approach are:

  1. The actions are defined in the command set.
  2. The controller links to the CommandSet via the getCommandSet() method.
  3. The business logic of the actions is still defined in the controller.
  4. The container uses a factory to create the controller.

As in the previous case, the container can create the controller via a configuration file:

figure 5

or by adding it as a dependency:

figure 6

In both cases, the container is of the CommandableCloudFunction type, which allows it to utilize the Commandable pattern.

When using a configuration file to create the controller, the container looks like this:

export class MyCommandableCloudFunction extends CommandableCloudFunction {
    private _service: MyServcie;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._service = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
    }
}


Not available

Not available

And, when adding the controller as a dependency, it looks like this:

export class MyCommandableCloudFunction extends CommandableCloudFunction {
    private _service: MyServcie;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml";
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '*'))
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = this._dependencyResolver.getOneRequired('service');
    }
}


Not available

Not available

In both cases, the controller is registered in the factory:

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service", "1.0"), MyServcie);
    }
}


Not available

Not available

Since we’re going to use the Commandable pattern, the controller needs to implement the ICommandable interface, which declares the getCommandSet method. Additionally, the controller must include the implementation of the actions’ business logic. Such a controller’s code will look like this:

export class MyServcie implements IReferenceable, ICommandable {
    private commandSet: CommandSet;

    public setReferences(references: IReferences): void {
    }

    public getCommandSet(): CommandSet {
        if (this.commandSet == null) {
            this.commandSet = new MyCommandSet(this);
        }

        return this.commandSet;
    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}


Not available

Not available

The CommandSet in this case would be:

export class MyCommandSet extends CommandSet {
    private _service: MyServcie;

    public constructor(service: MyServcie) {
        super();
        this._service = service;
        this.addCommand(this.makeGreeting());
    }

    private makeGreeting(): Command {
        return new Command(
            "greetings",
            new ObjectSchema(true).withRequiredProperty("name", TypeCode.String),
            async (ctx: Context, args: Parameters) => {
                let name = args.getAsString("name");
                return await this._service.greetings(name);
            }
        );
    }
}


Not available

Not available

After combining everything, the final code is:

import { CommandableCloudFunction } from "pip-services4-gcp-node";
import { TypeCode } from "pip-services4-commons-node";
import { ObjectSchema } from "pip-services4-data-node";
import { Descriptor, Factory, IReferenceable, IReferences, Context } from "pip-services4-components-node";
import { CommandSet, Command, ICommandable } from "pip-services4-rpc-node";

export class MyCommandSet extends CommandSet {
    private _service: MyServcie;

    public constructor(service: MyServcie) {
        super();
        this._service = service;
        this.addCommand(this.makeGreeting());
    }

    private makeGreeting(): Command {
        return new Command(
            "greetings",
            new ObjectSchema(true).withRequiredProperty("name", TypeCode.String),
            async (ctx: Context, args: Parameters) => {
                let name = args.getAsString("name");
                return await this._service.greetings(name);
            }
        );
    }
}

export class MyServcie implements IReferenceable, ICommandable {
    private commandSet: CommandSet;

    public setReferences(references: IReferences): void {
    }

    public getCommandSet(): CommandSet {
        if (this.commandSet == null) {
            this.commandSet = new MyCommandSet(this);
        }

        return this.commandSet;
    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service", "1.0"), MyServcie);
    }
}

export class MyCommandableCloudFunction extends CommandableCloudFunction {
    private _service: MyServcie;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = "./config.yaml";
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '*'))
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._service = this._dependencyResolver.getOneRequired('service');
        // Comment out the next line of code when using dependency resolver, uncomment for configuration file
        this._service = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
    }
}


Not available

Not available

Microservice requires a service layer

This section expands on the previous two cases by adding a service layer to expose the actions contained in the microservice. As a result, actions can now be registered in the service (instead of the container) or defined in a CommandSet component. As in the previous examples, actions are represented by the greetings() method.

Registering actions in the service

In this case, the microservice has a service and a controller, both of which get packaged into the container. As such, it has the following characteristics:

  1. Actions are registered in the service.
  2. The business logic of the actions is defined in the controller.
  3. The container uses a factory to create the controller and the service.

Similar to the previous examples, the service and the controller can be instantiated by the container via a factory that obtains information from a configuration file:

figure 7

or by defining them as the container’s dependencies:

figure 8

In the first case, the container looks like this:

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = references.getOneRequired(new Descriptor("mygroup", "controller", "gcp-function", "function", '*'));
        this._service = references.getOneRequired(new Descriptor("mygroup", "serice", "default", "service", '*'));
    }
}


Not available

Not available

with the configuration file containing the descriptors of the service and controller:

# Console logger
- descriptor: "pip-services:logger:console:default:1.0"
  level: "trace"  
# Service
- descriptor: "mygroup:service:default:service:1.0"
# Controller
- descriptor: "mygroup:controller:gcp-function:function:1.0"

And, in the second case, the container adds the dependencies via the dependency resolver:

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
        this._dependencyResolver.put('controller', new Descriptor("mygroup", "controller", "gcp-function", "function", '*'));
        this._dependencyResolver.put('service', new Descriptor("mygroup", "serice", "default", "service", '*'));
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = this._dependencyResolver.getOneRequired('controller');
        this._service = this._dependencyResolver.getOneRequired('service');
    }
}


Not available

Not available

In either case, both components must be registered in the factory:

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "serice", "default", "service", "1.0"), MyService);
        this.registerAsType(new Descriptor("mygroup", "controller", "gcp-function", "function", "1.0"), MyCloudController);
    }
}


Not available

Not available

The actions are registered in the service, which also adds the controller as a dependency:

export class MyCloudController extends CloudFunctionController {
    private _service: MyService;
    private _headers = {
        'Content-Type': 'application/json'
    }

    public constructor() {
        super();
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '1.0'));
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._service = this._dependencyResolver.getOneRequired('service');
    }

    protected register(): void {
        this.registerAction(
            'greetings',
            new ObjectSchema(true)
                .withRequiredProperty('body',
                    new ObjectSchema()
                        .withRequiredProperty('name', TypeCode.String)
                ),
            async (req: Request, res: Response) => {
                let params = CloudFunctionRequestHelper.getParameters(req);
                let name = params.getAsStringWithDefault('name', 'default name');

                let result = await this._service.greeting(name);

                for (let key of Object.keys(this._headers))
                    res.headers.append(key, this._headers[key]);
                
                HttpResponseSender.sendResult(req, res, result);
            }
        );
    }
}


Not available

Not available

And the actions’ business logic is defined in the controller:

export class MyService implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}


Not available

Not available

After grouping everything together, the final code is:

import { CloudFunctionController, CloudFunctionRequestHelper, CloudFunction } from "pip-services4-gcp-node";
import { TypeCode } from "pip-services4-commons-node";
import { ObjectSchema } from "pip-services4-data-node";
import { Descriptor, Factory, IReferenceable, IReferences, Context } from "pip-services4-components-node";
import { HttpResponseSender } from "pip-services4-http-node";

export class MyCloudController extends CloudFunctionController {
    private _service: MyService;
    private _headers = {
        'Content-Type': 'application/json'
    }

    public constructor() {
        super();
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '1.0'));
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        // Comment out the next line of code when using a configuration file, uncomment to use the dependency resolver
        // this._service = this._dependencyResolver.getOneRequired('service');
        // Comment out the next line of code when using the dependency resolver, uncomment for configuration file
        this._service = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '1.0'))
    }

    protected register(): void {
        this.registerAction(
            'greetings',
            new ObjectSchema(true)
                .withRequiredProperty('body',
                    new ObjectSchema()
                        .withRequiredProperty('name', TypeCode.String)
                ),
            async (req: Request, res: Response) => {
                let params = CloudFunctionRequestHelper.getParameters(req);
                let name = params.getAsStringWithDefault('name', 'default name');

                let result = await this._service.greeting(name);

                for (let key of Object.keys(this._headers))
                    res.headers.append(key, this._headers[key]);
                
                HttpResponseSender.sendResult(req, res, result);
            }
        );
    }
}

export class MyService implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "serice", "default", "service", "1.0"), MyService);
        this.registerAsType(new Descriptor("mygroup", "controller", "gcp-function", "function", "1.0"), MyCloudController);
    }
}

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
        // Comment out the next two lines of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('controller', new Descriptor("mygroup", "controller", "gcp-function", "function", '*'))
        // this._dependencyResolver.put('service', new Descriptor(("mygroup", "serice", "default", "service", '*'))
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        // Comment out the next two lines of code when using a configuration file, uncomment to use the dependency resolver
        // this._controller = this._dependencyResolver.getOneRequired('controller');
        // this._service = this._dependencyResolver.getOneRequired('service');
        // Comment out the next two lines of code when using the dependency resolver, uncomment for configuration file
        this._controller = references.getOneRequired(new Descriptor("mygroup", "controller", "gcp-function", "function", '*'));
        this._service = references.getOneRequired(new Descriptor("mygroup", "serice", "default", "service", '*'));
    }
}



Not available

Not available

Using a CommandSet component

In this case, a CommandSet component containing the definitions of the actions is added to the structure of the microservice from the previous example. The characteristics of such a microservice are:

  1. The service is of the CommandableCloudService type, which has the added functionality of automatically generating the necessary operations for commands defined in ICommandable components.
  2. The controller links to the CommandSet via the getCommandSet() method.
  3. The required business logic of the actions is still defined in the controller.
  4. The container uses a factory to create the service and the controller.

As in the previous cases, the controller and the service can be instantiated by the container via a factory that obtains the necessary information from a configuration file:

figure 9

or by defining them as container dependencies:

figure 10

In the first case, the container looks like this:

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCommandableCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
        this._service = references.getOneRequired(new Descriptor('mygroup', 'controller', 'commandable-gcp-function', 'function', '*'));
    }
}


Not available

Not available

And loads a configuration file that includes the descriptors of the service and the controller:

# Service
- descriptor: "mygroup:service:default:service:1.0"
# Controller 
- descriptor: "mygroup:controller:commandable-gcp-function:function:1.0"

When considering the service and controller as dependencies of the container, the code is:

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCommandableCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '*'))
        this._dependencyResolver.put('controller', new Descriptor('mygroup', 'controller', 'commandable-gcp-function', 'function', '*'))
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._controller = this._dependencyResolver.getOneRequired('controller');
        this._service = this._dependencyResolver.getOneRequired('service');
    }
}


Not available

Not available

In both cases, the command set is:

export class MyCommandSet extends CommandSet {
    private _servcie: MyService;

    public constructor(controller: MyService) {
        super();
        this._servcie = controller;
        this.addCommand(this.makeGreeting());
    }

    private makeGreeting(): Command {
        return new Command(
            "greetings",
            new ObjectSchema(true).withRequiredProperty("name", TypeCode.String),
            async (ctx: Context, args: Parameters) => {
                let name = args.getAsString("name");
                return await this._servcie.greetings(name);
            }
        );
    }
}


Not available

Not available

Which connects to the controller via the getCommandSet() method. Thus, the controller, which contains the business logic of the actions, looks like this:

export class MyService implements IReferenceable, ICommandable {
    private commandSet: CommandSet;

    public setReferences(references: IReferences): void {
    }

    public getCommandSet(): CommandSet {
        if (this.commandSet == null) {
            this.commandSet = new MyCommandSet(this);
        }

        return this.commandSet;
    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}


Not available

Not available

In this case, the service is of the CommandableCloudService type, because this class contains the necessary functionality to work with the command set.

export class MyCommandableCloudController extends CommandableCloudFunctionController {
    public constructor() {
        super('mygroup');
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '1.0'));
    }
}


Not available

Not available

And the factory registers both the controller and the service:

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service", "1.0"), MyService);
        this.registerAsType(new Descriptor("mygroup", "controller", "commandable-gcp-function", "function", "1.0"), MyCommandableCloudController);
    }
}


Not available

Not available

Finally, after grouping everything together, we obtain the following microservice:

import { CommandableCloudFunctionController, CloudFunction } from "pip-services4-gcp-node";
import { TypeCode } from "pip-services4-commons-node";
import { ObjectSchema } from "pip-services4-data-node";
import { Descriptor, Factory, IReferenceable, IReferences, Context } from "pip-services4-components-node";
import { CommandSet, Command, ICommandable } from "pip-services4-rpc-node";

export class MyCommandSet extends CommandSet {
    private _servcie: MyService;

    public constructor(controller: MyService) {
        super();
        this._servcie = controller;
        this.addCommand(this.makeGreeting());
    }

    private makeGreeting(): Command {
        return new Command(
            "greetings",
            new ObjectSchema(true).withRequiredProperty("name", TypeCode.String),
            async (ctx: Context, args: Parameters) => {
                let name = args.getAsString("name");
                return await this._servcie.greetings(name);
            }
        );
    }
}

export class MyCommandableCloudController extends CommandableCloudFunctionController {
    public constructor() {
        super('mygroup');
        this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '1.0'));
    }
}


export class MyService implements IReferenceable, ICommandable {
    private commandSet: CommandSet;

    public setReferences(references: IReferences): void {
    }

    public getCommandSet(): CommandSet {
        if (this.commandSet == null) {
            this.commandSet = new MyCommandSet(this);
        }

        return this.commandSet;
    }

    public async greetings(name: string): Promise<string> {
        return "Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service", "1.0"), MyService);
        this.registerAsType(new Descriptor("mygroup", "controller", "commandable-gcp-function", "function", "1.0"), MyCommandableCloudController);
    }
}

export class MyCloudFunction extends CloudFunction {
    private _controller: MyCommandableCloudController;
    private _service: MyService;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
        // Comment out the next two lines of code when using a configuration file, uncomment to use the dependency resolver
        // this._dependencyResolver.put('service', new Descriptor('mygroup', 'service', 'default', 'service', '*'))
        // this._dependencyResolver.put('controller', new Descriptor('mygroup', 'controller', 'commandable-gcp-function', 'function', '*'))
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        // Comment out the next two lines of code when using a configuration file, uncomment to use the dependency resolver
        // this._controller = this._dependencyResolver.getOneRequired('controller');
        // this._service = this._dependencyResolver.getOneRequired('service');
        // Comment out the next two lines of code when using the dependency resolver, uncomment for configuration file
        this._controller = references.getOneRequired(new Descriptor('mygroup', 'service', 'default', 'service', '*'));
        this._service = references.getOneRequired(new Descriptor('mygroup', 'controller', 'commandable-gcp-function', 'function', '*'));
    }
}


Not available

Not available

Combining the basic examples

The former four cases can be combined and expanded in many different ways, providing solutions for complex business scenarios.

The next sections will examine two of such examples. The first considers a microservice with two controllers and actions registered in the container. The second adds a service, links it to one of the controllers, registers some of the actions in the service, and then registers the rest in the container.

Example 1

This example extends the first of the previously explained cases by adding a second controller. Its main characteristics are:

  1. The container registers two different actions, namely, greetings1() and greetings2().
  2. Each controller contains the business logic for only one of these actions.
  3. The controllers are added as dependencies of the container.

The following diagram and code show what this example looks like:

figure 11

import { CloudFunctionRequestHelper, CloudFunction } from "pip-services4-gcp-node";
import { Descriptor, Factory, IReferenceable, IReferences } from "pip-services4-components-node";
import { HttpResponseSender } from "pip-services4-http-node";

export class MyService1 implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting1(name: string): Promise<string> {
        return "greetings1: Hello, " + name + " !";
    }
}

export class MyService2 implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting2(name: string): Promise<string> {
        return "greetings2: Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service1", "1.0"), MyService1);
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service2", "1.0"), MyService2);
    }
}

export class MyCloudFunction extends CloudFunction {
    private _service1: MyService1;
    private _service2: MyService2;

    public constructor() {
        super("mygroup", "MyGroup");
        this._configPath = './config.yaml';
        this._dependencyResolver.put('service1', new Descriptor('mygroup', 'service', 'default', 'service1', '*'))
        this._dependencyResolver.put('service2', new Descriptor('mygroup', 'service', 'default', 'service2', '*'))
        this._factories.add(new MyFactory())
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._service1 = this._dependencyResolver.getOneRequired('service1');
        this._service2 = this._dependencyResolver.getOneRequired('service2');
    }

    private async action1(req: Request, res: Response): Promise<void> {
        let params = CloudFunctionRequestHelper.getParameters(req);
        let name = params.getAsStringWithDefault('name', 'default name');

        let result = await this._service1.greeting1(name);

        HttpResponseSender.sendResult(req, res, result);
    }

    private async action2(req: Request, res: Response): Promise<void> {
        let params = CloudFunctionRequestHelper.getParameters(req);
        let name = params.getAsStringWithDefault('name', 'default name');

        let result = await this._service2.greeting2(name);

        HttpResponseSender.sendResult(req, res, result);
    }

    protected register(): void {
        this.registerAction("greetings1", null, this.action1);
        this.registerAction("greetings2", null, this.action2);
    }
}


Not available

Not available

Example 2

This example extends the previous one by adding a service layer linked to one of the controllers. As such, it combines two basic cases: a microservice without a service layer and a microservice with a service layer. It has the following characteristics:

  1. Actions linked to the first controller are registered in the service.
  2. Actions linked to the second controller are registered in the container.
  3. The controllers and the service are created via the factory using information stored in the configuration file.

The following class diagram and code show what this example looks like:

figure 12

import { CloudFunctionRequestHelper, CloudFunction, CloudFunctionController } from "pip-services4-gcp-node";
import { Descriptor, Factory, IReferenceable, IReferences } from "pip-services4-components-node";
import { HttpResponseSender } from "pip-services4-http-node";

export class MyCloudFunctionController extends CloudFunctionController {
    private _service: MyService1;

    private _headers = {
        'Content-Type': 'application/json'
    };

    public constructor() {
        super("myservice");
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._service = references.getOneRequired(new Descriptor("mygroup", "service", "*", "service1", "1.0"));
    }

    private async action(req: Request, res: Response): Promise<void> {
        let params = CloudFunctionRequestHelper.getParameters(req);
        let name = params.getAsStringWithDefault('name', 'default name');

        let result = await this._service.greeting1(name);

        HttpResponseSender.sendResult(req, res, result);
    }

    protected register(): void {
        this.registerAction("greetings1", null, this.action);
    }
}

export class MyService1 implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting1(name: string): Promise<string> {
        return "Greetings from service: Hello, " + name + " !";
    }
}

export class MyService2 implements IReferenceable {
    public setReferences(references: IReferences): void {

    }

    public async greeting2(name: string): Promise<string> {
        return "Greetings from container: Hello, " + name + " !";
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service1", "1.0"), MyService1);
        this.registerAsType(new Descriptor("mygroup", "service", "default", "service2", "1.0"), MyService2);
        this.registerAsType(new Descriptor("mygroup", "controller", "gcp-function", "*", "1.0"), MyCloudFunctionController)
    }
}

export class MyCloudFunction extends CloudFunction {
    private _service: MyService2;

    public constructor() {
        super("mygroup", "Mygroup service");
        this._configPath = "./config.yaml";
        this._factories.add(new MyFactory());
    }

    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._service = references.getOneRequired(new Descriptor("mygroup", "service", "*", "service2", "1.0"));
    }

    private async action(req: Request, res: Response): Promise<void> {
        let params = CloudFunctionRequestHelper.getParameters(req);
        let name = params.getAsStringWithDefault('name', 'default name');

        let result = await this._service.greeting2(name);

        HttpResponseSender.sendResult(req, res, result);
    }

    protected register(): void {
        this.registerAction('greetings2', null, this.action);
    }
}


Not available

Not available

Where the configuration file is:

# Console logger
- descriptor: "pip-services:logger:console:default:1.0"
  level: "trace"

# controller
- descriptor: "mygroup:controller:gcp-function:*:1.0"

# service 1
- descriptor: "mygroup:service:default:service1:1.0"

# service 2
- descriptor: "mygroup:service:default:service2:1.0"

Testing

After running an application with the help of a tool for example like Functions Framework for Python, the results can be seen by sending a test request to the container via HTTP. The following steps explain how this can be done:

Step 1: Create an instance of the container

function_service = MyCloudFunction()

Step 2: Create an instance of the handler

handler = function_service.get_handler()

Step 3: Run the tool

To see the results on a local machine run:

functions-framework --target handler --signature-type http --port 8080 --source program_name.py

where program_name.py is the name of the file containing the GCP function service.

Step 4: Call an action.

A specific action, such as ‘greetings’ in our previous examples, can be called by running a command similar to the following one:

curl -d '{"cmd": ""myservice.greetings1", "name": "Bob"}' -H "Content-Type: application/json" -X POST http://localhost:8080

After running the previous cURL command, the result will be displayed as part of the response that is received.

figure 13

Alternatively, a REST Client can be used to achieve the same result:

figure 14

Wrapping up

In this tutorial, we learned how to create a microservice, packaged in a Google Cloud Platform container. We saw four different scenarios with code examples. The following table summarizes these cases:

Without a service layer With a service layer
Manually registering the action
  • Actions are registered in the container.
  • The controller is created via a factory using information from a configuration file or by defining it as the container’s dependency.
  • Actions are registered in the service.
  • The controller and service are created via a factory using information from a configuration file or by defining them as the container’s dependencies.
Using a CommandSet component
  • A CommandSet component defines the actions.
  • The controller adds the getCommandSet() method and implements the ICommandable interface.
  • The business logic is added to the actions in the controller.
  • A commandable version of the container is used.
  • The controller is created via a factory using information from a configuration file or by defining it as the container’s dependency.
  • A CommandSet component defines the actions.
  • The controller adds the getCommandSet() method and implements the ICommandable interface.
  • The business logic is added to the actions in the controller.
  • A commandable version of the service is used.
  • The controller and service are created via a factory using information from a configuration file or by defining them as the container’s dependencies.