Skip to content

Latest commit

 

History

History
724 lines (521 loc) · 20.1 KB

cds-connect.md

File metadata and controls

724 lines (521 loc) · 20.1 KB
shorty status
cds.connect
released

Connecting to Required Services

Services frequently consume other services, which could be local services served by the same process, or external services, for example consumed through OData. The latter include database services. In all cases use cds.connect to connect to such services, for example, from your:

Configuring Required Services {#cds-env-requires }

To configure required remote services in Node.js, simply add respective entries to the cds.requires sections in your package.json or in .cdsrc.json (omitting the cds. prefix). These configurations are constructed as follows:

"cds": {
  "requires": {
    "ReviewsService": {
      "kind": "odata", "model": "@capire/reviews"
    },
    "OrdersService": {
      "kind": "odata", "model": "@capire/orders"
    },
  }
}

Entries in this section tell the service loader to not serve that service as part of your application, but expects a service binding at runtime in order to connect to the external service provider. The options are as follows:

cds.requires.<srv>.impl

Service implementations are ultimately configured in cds.requires like that:

"cds": { "requires": {
  "some-service": { "impl": "some/node/module/path" },
  "another-service": { "impl": "./local/module/path" }
}}

Given that configuration, cds.connect.to('some-service') would load the specific service implementation from some/node/module/path. Prefix the module path in impl with ./ to refer to a file relative to your project root.

cds.requires.<srv>.kind

As service configurations inherit from each other along kind chains, we can refer to default configurations shipped with @sap/cds, as you commonly see that in our cap/samples, like so:

"cds": { "requires": {
  "db": { "kind": "sqlite" },
  "remote-service": { "kind": "odata" }
}}

This is backed by these default configurations:

"cds": { "requires": {
  "sqlite": { "impl": "[...]/sqlite/service" },
  "odata": { "impl": "[...]/odata/service" },
}}

Run cds env get requires to see all default configurations. Run cds env get requires.db.impl to see the impl used for your database.

Given that configuration, cds.connect.to('db') would load the generic service implementation.

Learn more about cds.env.{.learn-more}

cds.requires.<srv>.model

Specify (imported) models for remote services in this property. This allows the service runtime to reflect on the external API and add generic features. The value can be either a single string referring to a CDS model source, resolved as absolute node module, or relative to the project root, or an array of such.

"cds": { "requires": {
  "remote-service": { "kind": "odata", "model":"some/imported/model" }
}}

Upon bootstrapping, all these required models will be loaded and compiled into the effective cds.model as well.

cds.requires.<srv>.service

If you specify a model, then a service definition for your required service must be included in that model. By default, the name of the service that is checked for is the name of the required service. This can be overwritten by setting service inside the required service configuration.

"cds": { "requires": {
  "remote-service": { "kind": "odata", "model":"some/imported/model", "service": "BusinessPartnerService" }
}}

The example specifies service: 'BusinessPartnerService', which results in a check for a service called BusinessPartnerService instead of remote-service in the model loaded from some/imported/model.

cds.requires.<srv>.credentials

Specify the credentials to connect to the service. Credentials need to be kept secure and should not be part of a configuration file.

Connecting to Required Services { #cds-connect-to }

cds. connect.to () {.method}

Declaration:

async function cds.connect.to (
  name : string,  // reference to an entry in `cds.requires` config
  options : {
    kind : string // reference to a preset in `cds.requires.kinds` config
    impl : string // module name of the implementation
  }
)

Use cds.connect.to() to connect to services configured in a project's cds.requires configuration. Usually such services are remote services, which in turn can be mocked locally. Here's an example:

::: code-group

{"cds":{
  "requires":{
    "db": { "kind": "sqlite", "credentials": { "url":"db.sqlite" }},
    "ReviewsService": { "kind": "odata-v4" }
  }
}}

:::

const ReviewsService = cds.connect.to('ReviewsService')
const db = cds.connect.to('db')

Argument options allows to pass options programmatically, and thus create services without configurations, for example:

const db2 = cds.connect.to ({
  kind: 'sqlite', credentials: { url: 'db2.sqlite' }
})

In essence, cds.connect.to() works like that:

let o = { ...cds.requires[name], ...options }
let csn = o.model ? await cds.load(o.model) : cds.model
let Service = require (o.impl) //> a subclass of cds.Service
let srv = new Service (name, csn, o)
return srv.init() ?? srv

cds.connect.to (name, options?) → service

Connects to a required service and returns a Promise resolving to a corresponding Service instance. Subsequent invocations with the same service name all return the same instance.

const srv = await cds.connect.to ('some-service')
const { Books } = srv.entities
await srv.run (SELECT.from(Books))

Arguments:

Caching:

Service instances are cached in cds.services, thus subsequent connects with the same service name return the initially connected one. As services constructed by cds.serve are registered with cds.services as well, a connect finds and returns them as local service connections.

If ad-hoc options are provided, the instance is not cached.

cds.connect.to (options) → service

Ad-hoc connection (→ only for tests):

cds.connect.to ({ kind:'sqlite', credentials:{database:'my.db'} })

cds.connect.to ('<kind>:<url>') → service

This is a shortcut for ad-hoc connections.

For example:

cds.connect.to ('sqlite:my.db')

is equivalent to:

cds.connect.to ({kind: 'sqlite', credentials:{database:'my.db'}})

Service Bindings {#service-bindings}

A service binding connects an application with a cloud service. For that, the cloud service's credentials need to be injected in the CDS configuration:

{
  "requires": {
    "db": {
      "kind": "hana",
      "credentials": { /* from service binding */ }
    }
  }
}

You specify the credentials to be used for a service by using one of the following:

  • Environment variables
  • File system
  • Auto binding

What to use depends on your environment.

In Cloud Foundry {#bindings-in-cloud-platforms}

Find general information about how to configure service bindings in Cloud Foundry:

Cloud Foundry uses auto configuration of service credentials through the VCAP_SERVICES environment variable.

Through VCAP_SERVICES env var {#vcap_services}

When deploying to Cloud Foundry, service bindings are provided in VCAP_SERVICES process environment variables, which is JSON-stringified array containing credentials for multiple services. The entries are matched to the entries in cds.requires as follows, in order of precedence:

  1. The service's name is matched against the name property of VCAP_SERVICE entries
  2. The service's name is matched against the binding_name property
  3. The service's name is matched against entries in the tags array
  4. The service's kind is matched against entries in the tags array
  5. The service's kind is matched against the label property, for example, 'hana'
  6. The service's kind is matched against the type property (The type property is only relevant for servicebinding.io bindings)
  7. The service's vcap.name is matched against the name property

All the config properties found in the first matched entry will be copied into the cds.env.requires.<i>\<srv\></i>.credentials property.

Here are a few examples:

CAP config VCAP_SERVICES
{
  "cds": {
    "requires": {
      "db": { ... }
    }
  }
}
{
  "VCAP_SERVICES": {
    "hana": [{
      "name": "db", ...
    }]
  }
}
{
  "cds": {
    "requires": {
      "db": { "kind": "hana" }
    }
  }
}
{
  "VCAP_SERVICES": {
    "hana": [{
      "label": "hana", ...
    }]
  }
}
{
  "cds": {
    "requires": {
      "db": {
        "vcap": { "name": "myDb" }
      }
    }
  }
}
{
  "VCAP_SERVICES": {
    "hana": [{
      "name": "myDb",
      ...
    }]
  }
}

In Kubernetes / Kyma { #in-kubernetes-kyma}

CAP supports servicebinding.io service bindings and SAP BTP service bindings created by the SAP BTP Service Operator.

  1. Specify a root directory for all service bindings using SERVICE_BINDING_ROOT environment variable:

    spec:
      containers:
      - name: bookshop-srv
        env:
        ...
        - name: SERVICE_BINDING_ROOT
          value: /bindings
  2. Create service bindings

    Use the ServiceBinding custom resource of the SAP BTP Service Operator to create bindings to SAP BTP services:

    apiVersion: services.cloud.sap.com/v1alpha1
    kind: ServiceBinding
    metadata:
      name: bookshop-xsuaa-binding
    spec:
        serviceInstanceName: bookshop-xsuaa-binding
        externalName: bookshop-xsuaa-binding
        secretName: bookshop-xsuaa-secret

    Bindings to other services need to follow the servicebinding.io workload projection specification.

  3. Mount the secrets of the service bindings underneath the root directory:

    spec:
      containers:
      - name: bookshop-srv
        ...
        volumeMounts:
        - name: bookshop-auth
          mountPath: "/bindings/auth"
          readOnly: true
      volumes:
      - name: bookshop-auth
        secret:
          secretName: bookshop-xsuaa-secret

    The secretName property refers to an existing Kubernetes secret, either manually created or by the ServiceBinding resource. The name of the sub directory (auth in the example) is recognized as the binding name.

CAP services receive their credentials from these bindings as if they were provided using VCAP_SERVICES.

Through environment variables {#env-service-bindings}

All values of a secret can be added as environment variables to a pod. A prefix can be prepended to each of the environment variables. To inject the values from the secret in the right place of your CDS configuration, you use the configuration path to the credentials object of the service as the prefix:

cds_requires_<your service>_credentials_

Please pay attention to the underscore ("_") character at the end of the prefix.

Example:

  spec:
    containers:
    - name: app-srv
      ...
      envFrom:
        - prefix: cds_requires_db_credentials_
          secretRef:
            name: app-db

::: warning For the configuration path, you must use the underscore ("_") character as delimiter. CAP supports dot (".") as well, but Kubernetes won't recognize variables using dots. Your service name mustn't contain underscores. :::

Through the file system {#file-system-service-bindings}

CAP can read configuration from a file system by specifying the root path of the configuration in the CDS_CONFIG environment variable.

Set CDS_CONFIG to the path that should serve as your configuration root, for example: /etc/secrets/cds.

Put the service credentials into a path that is constructed like this:

<configuration root>/requires/<your service>/credentials

Each file will be added to the configuration with its name as the property name and the content as the value. If you have a deep credential structure, you can add further sub directories or put the content in a file as a JSON array or object.

For Kubernetes, you can create a volume with the content of a secret and mount it on your container.

Example:

  spec:
    volumes:
      - name: app-db-secret-vol
        secret:
          secretName: app-db
    containers:
    - name: app-srv
      ...
      env:
        - name: CDS_CONFIG
          value: /etc/secrets/cds
      volumeMounts:
        - name: app-db-secret-vol
          mountPath: /etc/secrets/cds/requires/db/credentials
          readOnly: true

Provide Service Bindings (VCAP_SERVICES) {#provide-service-bindings}

If your application runs in a different environment than Cloud Foundry, the VCAP_SERVICES env variable is not available. But it may be needed by some libraries, for example the SAP Cloud SDK.

By enabling the CDS feature features.emulate_vcap_services, the VCAP_SERVICES env variable will be populated from your configured services.

For example, you can enable it in the package.json file for your production profile:

{
  "cds": {
    "features": {
      "[production]": {
        "emulate_vcap_services": true
      }
    }
  }
}

::: warning This is a backward compatibility feature.
It might be removed in a next major CAP version. :::

Each service that has credentials and a vcap.label property is put into the VCAP_SERVICES env variable. All properties from the service's vcap object will be taken over to the service binding.

The vcap.label property is pre-configured for some services used by CAP.

For example, for the XSUAA service you only need to provide credentials and the service kind:

{
  "requires": {
    "auth": {
      "kind": "xsuaa",
      "credentials": {
        "clientid": "cpapp",
        "clientsecret": "dlfed4XYZ"
      }
    }
  }
}

The VCAP_SERVICES variable is generated like this:

{
  "xsuaa": [
    {
      "label": "xsuaa",
      "tags": [ "auth" ],
      "credentials": {
        "clientid": "cpapp",
        "clientsecret": "dlfed4XYZ"
      }
    }
  ]
}

The generated value can be displayed using the command:

cds env get VCAP_SERVICES --process-env

A list of all services with a preconfigured vcap.label property can be displayed with this command:

cds env | grep vcap.label

You can include your own services by configuring vcap.label properties in your CAP configuration.

For example, in the package.json file:

{
  "cds": {
    "requires": {
      "myservice": {
        "vcap": {
          "label": "myservice-label"
        }
      }
    }
  }
}

The credentials can be provided in any supported way. For example, as env variables:

cds_requires_myservice_credentials_user=test-user
cds_requires_myservice_credentials_password=test-password

The resulting VCAP_SERVICES env variable looks like this:

{
  "myservice-label": [
    {
      "label": "myservice-label",
      "credentials": {
        "user": "test-user",
        "password": "test-password"
      }
    }
  ]
}

Hybrid Testing

In addition to the static configuration of required services, additional information, such as urls, secrets, or passwords are required to actually send requests to remote endpoints. These are dynamically filled into property credentials from process environments, as explained in the following.

cds.requires.<srv>.credentials

All service binding information goes into this property. It's filled from the process environment when starting server processes, managed by deployment environments. Service bindings provide the details about how to reach a required service at runtime, that is, providing requisite credentials, most prominently the target service's url.

For development purposes, you can pass them on the command line or add them to a .env or default-env.json file as follows:

# .env file
cds.requires.remote-service.credentials = { "url":"http://...", ... }

::: warning ❗ Never add secrets or passwords to package.json or .cdsrc.json! General rule of thumb: .credentials are always filled (and overridden) from process environment on process start. :::

One prominent exception of that, which you would frequently add to your package.json is the definition of a database file for persistent sqlite database during development:

  "cds": { "requires": {
    "db": {
      "kind": "sql",
      "[development]": {
        "kind": "sqlite",
        "credentials": {
          "database": "db/bookshop.db"
        }
      }
    }
  }}

Basic Mechanism {#bindings-via-cds-env}

The CAP Node.js runtime expects to find the service bindings in cds.env.requires.

  1. Configured required services constitute endpoints for service bindings.

    "cds": {
      "requires": {
        "ReviewsService": {...},
       }
    }
  2. These are made available to the runtime via cds.env.requires.

    const { ReviewsService } = cds.env.requires
  3. Service Bindings essentially fill in credentials to these entries.

    const { ReviewsService } = cds.env.requires
    ReviewsService.credentials = {
      url: "http://localhost:4005/reviews"
    }

The latter is appropriate in test suites. In productive code, you never provide credentials in a hard-coded way. Instead, use one of the options presented in the following sections.

Through .cdsrc-private.json File for Local Testing

Learn more about hybrid testing using .cdsrc-private.json.

{
  "requires": {
    "ReviewsService": {
      "credentials": {
        "url": "http://localhost:4005/reviews"
      }
    },
    "db": {
      "credentials": {
        "database": "sqlite.db"
      }
    }
  }
}

::: warning Make sure that the .cdsrc-private.json file is not checked into your project. :::

Through process.env Variables {#bindings-via-process-env}

You could pass credentials as process environment variables, for example in ad-hoc tests from the command line:

export cds_requires_ReviewsService_credentials_url=http://localhost:4005/reviews
export cds_requires_db_credentials_database=sqlite.db
cds watch fiori

In .env Files for Local Testing

Add environment variables to a local .env file for repeated local tests:

cds.requires.ReviewsService.credentials = { "url": "http://localhost:4005/reviews" }
cds.requires.db.credentials.database = sqlite.db

Never check in or deploy such .env files!

Importing Service APIs

Mocking Required Services