Component configuration
- by Aleksey Dvoykin
Introduction
The Pip.Services Toolkit offers a simple but very flexible mechanism for component configuration. Configurations can be loaded from various sources - configuration files, command line parameters, environment variables, configuration services, etc. Once loaded, they are passed to the specific component, which configures itself accordingly. In this recipe, we’ll be taking a look at this mechanism’s capabilities and how it can be utilized.
Configuration
The configurable interface
A component can be made configurable by adding the IConfigurable interface and implementing its configure method. This method will be called by the container right after container creation, with the loaded configuration being passed as a parameter.
interface IConfigurable {
configure(config: ConfigParams): void;
}
import (
"context"
conf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
)
type IConfigurable interface {
Configure(ctx context.Context, config *conf.ConfigParams)
}
See: IConfigurable
from abc import ABC
from pip_services4_components.config import ConfigParams
class IConfigurable(ABC):
def configure(self, config: ConfigParams):
raise NotImplementedError('Method from interface definition')
The ConfigParams object
The only parameter that is passed to the configure method is ConfigParams object. Simply put - this is a map that allows us to get a configuration parameter value by its corresponding key. Although various programming languages have unique syntax for initializing maps and objects, ConfigParams support initialization that is independent of the language being used.
import { ConfigParams } from "pip-services4-components-node"
let config = ConfigParams.fromTuples(
"param1", 123,
"param2", "2020-01-01T11:00:00.0Z"
);
import (
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
)
config := cconfig.NewConfigParamsFromTuples(
"param1", 123,
"param2", "2020-01-01T11:00:00.0Z",
)
See: ConfigParams
config = ConfigParams.from_tuples(
"param1", 123,
"param2", "2020-01-01T11:00:00.0Z"
)
ConfigParams also provide some additional functionality. All keys and values are stored as strings, but ConfigParams supports performing data type conversion when extracting values. Another option available is the opportunity to set default values.
let param1 = config.getAsInteger("param1");
let param2 = config.getAsDateTimeWithDefault("param2", new Date());
param1 := config.GetAsInteger("param1")
param2 := config.GetAsDateTimeWithDefault("param2", time.Now())
from datetime import datetime
param1 = config.get_as_integer("param1")
param2 = config.get_as_datetime_with_default("param2", datetime.now())
The parameter kets can have a complex structure, grouped by sections using dot notation. ConfigParams can be used to work with entire sections as well.
let configWithSections = ConfigParams.fromTuples(
"param1", 123,
"options.param1", "ABC",
"options.param2", "XYZ"
);
let options = configWithSections.getSection("options");
configWithSections := cconfig.NewConfigParamsFromTuples(
"param1", 123,
"options.param1", "ABC",
"options.param2", "XYZ",
)
options := configWithSections.GetSection("options")
config_with_sections = ConfigParams.from_tuples(
"param1", 123,
"options.param1", "ABC",
"options.param2", "XYZ"
)
options = config_with_sections.get_section("options")
Setting a default configuration
Another helpful option is the ability to set a default configuration.
let defaultConfig = ConfigParams.fromTuples(
"param1", 1,
"param2", "Default Value"
);
config = config.setDefaults(defaultConfig);
defaultConfig := cconfig.NewConfigParamsFromTuples(
"param1", 1,
"param2", "Default Value",
)
config = config.SetDefaults(defaultConfig)
default_config = ConfigParams.from_tuples(
"param1", 1,
"param2", "Default Value"
)
config = config.set_defaults(default_config)
Serializing the ConfigParams object
Lastly, ConfigParams objects can be serialized/deserialized to/from JSON, YAML, or a plain string.
let anotherConfig = ConfigParams.fromString("param1=123;param2=ABC");
anotherConfig := cconfig.NewConfigParamsFromString("param1=123;param2=ABC")
See: Commons module’s
another_config = ConfigParams.from_string("param1=123;param2=ABC")
To read more about what functionality is available through ConfigParams, be sure to check out the Commons documentation.
Example
Below is an example of a configurable component:
export class DataController implements IConfigurable {
private _max_page_size: number = 5;
public constructor() { }
public configure(config: ConfigParams): void {
this._max_page_size = config.getAsIntegerWithDefault('max_page_size', this._max_page_size);
}
public getData(ctx: Context, filter: FilterParams, paging: PagingParams): Promise<DataPage<MyData>> {
paging.take = Math.min(paging.take, this._max_page_size);
// Get data using max page size constraint.
}
}
import (
"context"
"math"
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
cquery "github.com/pip-services4/pip-services4-go/pip-services4-data-go/query"
)
type DataService struct {
_maxPageSize int
}
func NewDataService() *DataService {
return &DataService{
_maxPageSize: 5,
}
}
func (c *DataService) Configure(ctx context.Context, config *cconfig.ConfigParams) {
c._maxPageSize = config.GetAsIntegerWithDefault("max_page_size", c._maxPageSize)
}
func (c *DataService) GetData(ctx context.Context, correlationId string, filter *cquery.FilterParams, paging *cquery.PagingParams) (page *cquery.DataPage, err error) {
paging.Take = int64(math.Min(float64(paging.Take), float64(c._maxPageSize)))
// Get data using max page size constraint.
return page, err
}
See: Commons module’s
from pip_services4_data.query import FilterParams, PagingParams, DataPage
class DataController(IConfigurable):
__max_page_size: int = 5
def configure(self, config: ConfigParams):
self.__max_page_size = config.get_as_integer_with_default('max_page_size', self.__max_page_size)
def get_data(self, correlation_id: str, filter: FilterParams, paging: PagingParams) -> DataPage:
paging.take = min(paging.take, self.__max_page_size) # Get data using max page size constraint.
Manual configuration
Manual configuration can be done in the following manner:
let component = new DataController();
let config = ConfigParams.fromTuples("max_page_size", 100);
component.configure(config);
component := configurationexample.NewDataService()
config := cconfig.NewConfigParamsFromTuples("max_page_size", 100)
component.Configure(context.Background(), config)
component = DataController()
config = ConfigParams.from_tuples("max_page_size", 100)
component.configure(config)
However, a component’s configuration is usually stored in the microservice’s configuration file. The configure method will receive the parameters for the specific component (in the example below - everything between the component’s descriptor and the next descriptor or end of the file). To get more info on microservice configuration, read our Component Container recipe.
...
# Controller
- descriptor: "beacons:controller:default:default:1.0"
max_page_size: 10
...
Resolvers
The NameResolver and OptionsResolver classes are helper classes that simplify the use of configurations.
NameResolver
NameResolver is a helper class that allows extraction of the component’s name from the configuration parameters. The name can be stored as a “id”/“name” parameter or inside a component’s descriptor.
Below is a simple example of how it can be used:
import { ConfigParams, NameResolver } from "pip-services4-components-node"
let config = ConfigParams.fromTuples(
"descriptor", "myservice:connector:aws:connector1:1.0",
"param1", "ABC",
"param2", 123
);
let name = NameResolver.resolve(config); // Result: connector1
config := cconfig.NewConfigParamsFromTuples(
"descriptor", "myservice:connector:aws:connector1:1.0",
"param1", "ABC",
"param2", 123,
)
name := cconfig.NameResolver.Resolve(config) // Result: connector1
See: NameResolver, OptionsResolver
from pip_services4_components.config import NameResolver
config = ConfigParams.from_tuples(
"descriptor", "myservice:connector:aws:connector1:1.0",
"param1", "ABC",
"param2", 123
)
name = NameResolver.resolve(config) # Result: connector1
OptionsResolver
OptionsResolver is a helper class that extracts parameters from the “options” configuration section.
let config = ConfigParams.fromTuples(
...
"options.param1", "ABC",
"options.param2", 123
);
let options = OptionsResolver.resolve(config); // Result: param1=ABC;param2=123
config := cconfig.NewConfigParamsFromTuples(
...
"options.param1", "ABC",
"options.param2", 123,
)
options := cconfig.OptionsResolver.Resolve(config) // Result: param1=ABC;param2=123
from pip_services4_components.config import OptionsResolver
config = ConfigParams.from_tuples(
#...
"options.param1", "ABC",
"options.param2", 123
)
options = OptionsResolver.resolve(config) # Result: param1=ABC;param2=123
Configuration readers
Configuration parameters can be stored in microservice configurations, configuration files, or in configuration services. To help with configuration extraction, the Pip.Services Toolkit offers two special ConfigReader components. The interface for these components is defined in the Components module.
interface IConfigReader {
readConfig(context: IContext, parameters: ConfigParams): Promise<ConfigParams>;
addChangeListener(listener: INotifiable): void;
removeChangeListener(listener: INotifiable): void;
}
import (
"context"
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
)
type IConfigReader interface {
ReadConfig(ctx context.Context, correlationId string, parameters *cconfig.ConfigParams) (*cconfig.ConfigParams, error)
}
See: Components
from typing import Optional
class IConfigReader(ABC):
def read_config_(self, correlation_id: Optional[str], parameters: ConfigParams) -> ConfigParams:
raise NotImplementedError('Method from interface definition')
MemoryConfigReader
The MemoryConfigReader is a ConfigReader that stores configuration data in memory.
let config = ConfigParams.fromTuples(
"connection.host", "localhost",
"connection.port", "8080"
);
let configReader = new MemoryConfigReader();
configReader.configure(config);
let parameters = ConfigParams.fromValue(process.env);
let result = await configReader.readConfig(ctx, parameters);
// Result: connection.host=localhost;connection.port=8080
import (
"context"
"os"
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
creader "github.com/pip-services4/pip-services4-go/pip-services4-config-go/config"
)
config := cconfig.NewConfigParamsFromTuples(
"connection.host", "localhost",
"connection.port", "8080",
)
configReader := creader.NewEmptyMemoryConfigReader()
configReader.Configure(context.Background(), config)
parameters := cconfig.NewConfigParamsFromValue(os.Args)
configReader.ReadConfig(context.Background(), parameters) // Result: connection.host=localhost;connection.port=8080
See: MemoryConfigReader, ConfigReader
import sys
from pip_services4_config.config import MemoryConfigReader
config = ConfigParams.from_tuples(
"connection.host", "localhost",
"connection.port", "8080"
)
config_reader = MemoryConfigReader()
config_reader.configure(config)
parameters = ConfigParams.from_value(sys.argv)
config_reader.read_config_("123", parameters) # Result: connection.host=localhost;connection.port=8080
JsonConfigReader
The JsonConfigReader is a ConfigReader that can read configurations from a JSON file.
{ "key1": "{{KEY1_VALUE}}", "key2": "{{KEY2_VALUE}}" }
let configReader = new JsonConfigReader("config.json");
let parameters = ConfigParams.fromTuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC");
let result = await configReader.readConfig(ctx, parameters); // Result: key1=123;key2=ABC
import (
"context"
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
creader "github.com/pip-services4/pip-services4-go/pip-services4-config-go/config"
)
configReader := creader.NewJsonConfigReader("config.json")
parameters := cconfig.NewConfigParamsFromTuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC")
configReader.ReadConfig(context.Background(), parameters) // Result: key1=123;key2=ABC
See: JsonConfigReader
from pip_services4_config.config import JsonConfigReader
configReader = JsonConfigReader("config.json")
parameters = ConfigParams.from_tuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC")
configReader.read_config_("123", parameters) # Result: key1=123;key2=ABC
YamlConfigReader
The YamlConfigReader is a ConfigReader that can read configurations from a YAML file.
key1: "1234"
key2: "ABCD"
let configReader = new YamlConfigReader("config.yml");
let parameters = ConfigParams.fromTuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC");
let result = await configReader.readConfig(ctx, parameters); // Result: key1=1234;key2=ABCD
import (
"context"
cconfig "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
creader "github.com/pip-services4/pip-services4-go/pip-services4-config-go/config"
)
configReader := creader.NewYamlConfigReader("config.yml")
parameters := cconfig.NewConfigParamsFromTuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC")
configReader.ReadConfig(context.Background(), parameters) // Result: key1=123;key2=ABC
See: YamlConfigReader
from pip_services4_config.config import YamlConfigReader
configReader = YamlConfigReader("config.yml")
parameters2 = ConfigParams.from_tuples("KEY1_VALUE", 123, "KEY2_VALUE", "ABC")
configReader.read_config_("123", parameters2) # Result: key1=1234;key2=ABCD