Resources
Resources are Namespace's solution for managing dependencies. They can model databases, caches, queues, etc.
Servers can depend on resources, but also resources can require other resources. For example, an application server might depend on a database, but the database, in turn, depends on a database cluster.
Configuration
To configure a resource, one has to specify the following:
- its resource class (the type definition of the resource)
- a provider that implements the resource class
- the intent to produce the resource from (defined by the resource class)
- potentially other resources that this resource depends on
For example, if we want to use an S3 bucket in our application, we could define it as follows:
resources: {
dataBucket: {
class: "namespacelabs.dev/foundation/library/storage/s3:Bucket"
provider: "namespacelabs.dev/foundation/library/oss/minio"
intent: {
bucketName: "my-test-bucket"
}
}
}
First, we select the resource class. The class defines the intent type that we need to provide for this resource.
Next, we pick a provider that implements this resource class, in this case, MinIO. There can be multiple providers for a resource class (e.g., Localstack and AWS S3).
Finally, we specify the intent to set the bucket name. From the intent, the MinIO provider will create a bucket for us.
The bucket is made available to us via an instance type defined by the resource class, for example:
{
"bucket_name": "my-test-bucket",
"url": "http://minio-api-service:9000",
"access_key": "generated-by-minio-provider",
"secret_access_key": "generated-by-minio-provider"
}
Resource classes
Resources have classes that define their API by specifying intent and instance types. The intent expresses which resource is required and is provided by you when defining the resource, either inline or dynamically computed. The instance represents the configuration of a created resource and is passed to the consumer.
For the example from above, an S3 bucket class has the following definition:
resourceClasses: {
"Bucket": {
intent: {
type: "library.storage.s3.BucketIntent"
source: "./api.proto"
}
produces: {
type: "library.storage.s3.BucketInstance"
source: "./api.proto"
}
}
}
and with api.proto
as
syntax = "proto3";
package library.storage.s3;
message BucketIntent {
string region = 1;
string bucket_name = 2;
}
message BucketInstance {
string region = 1;
string bucket_name = 2;
string url = 3;
string access_key = 4;
string secret_access_key = 5;
}
Providers
Resource providers are responsible for creating and managing a resource. As mentioned before, there can be multiple for the same resource class.
Providers run initializers on resource creation. An initializer is a binary that performs three steps:
- Consume the requested resource intent (passed via
--intent
) - Perform world-mutating actions to fulfill intent
- Return an instance representing the created resource
Namespace's framework provides
helper methods
for step 1 and 3.
Providers can depend on other resources by embedding them directly or requiring an input resource. Input resources need to be specified when using a provider in a resource definition.
Here is a sample MinIO provider for S3 buckets:
providers: {
"namespacelabs.dev/foundation/library/storage/s3:Bucket": {
initializedWith: imageFrom:
binary: "namespacelabs.dev/foundation/library/oss/minio/prepare"
resources: {
server: {
class: "namespacelabs.dev/foundation/library/runtime:Server"
intent: {
package_name: "namespacelabs.dev/foundation/library/oss/minio/server"
}
}
}
}
}
We start by selecting the resource class that the provider implements. This syntax allows a single package to contain multiple providers for different resource classes. The provider then specifies which initializer to run and which resources it requires. In this case, the MinIO provider adds the MinIO server to the stack but does not require user-defined resource inputs.
Namespace deploys resource dependencies before creating the current resource. In particular, Namespace will bring up MinIO server first. The bucket initializer only runs after MinIO server is ready to receive requests.
Resource lifetime is independent of their consumers. That means initializers will only be run on creation or if the resource intent or provider changes. Changes to the consumer of a resource do not affect it.