From aba1c06d0ac5a528d646aed5def12f21ef7f78ea Mon Sep 17 00:00:00 2001 From: Samia Nneji Date: Thu, 27 May 2021 23:04:29 +0100 Subject: [PATCH] Sink binding docs (reopened PR) (#3669) * Edit sinkbinding docs * adding attributes, need to add runtime contract * indent * document the env vars * try points: * add contract: * More edits to sinkbinding docs * Fix trailing whitespace * Update titles * Remove old sinkbinding topic * Add context * Update docs/eventing/sources/sinkbinding/_index.md Co-authored-by: Ashleigh Brennan * Update docs/eventing/sources/sinkbinding/_index.md Co-authored-by: Ashleigh Brennan * Update docs/eventing/sources/sinkbinding/_index.md Co-authored-by: Ashleigh Brennan * Apply feedback * making mkdocs formatting changes, changing index to README.md * Apply feedback pt 1 * Apply feedback pt 2 * Add feedback pt 3 * Modify instructions for setting inclusion mode * Tweak Co-authored-by: Scott Nichols Co-authored-by: Ashleigh Brennan Co-authored-by: Omer B --- docs/eventing/sources/sinkbinding.md | 244 --------------- docs/eventing/sources/sinkbinding/README.md | 19 ++ .../sources/sinkbinding/getting-started.md | 294 ++++++++++++++++++ .../eventing/sources/sinkbinding/reference.md | 171 ++++++++++ mkdocs.yml | 7 +- 5 files changed, 490 insertions(+), 245 deletions(-) delete mode 100644 docs/eventing/sources/sinkbinding.md create mode 100644 docs/eventing/sources/sinkbinding/README.md create mode 100644 docs/eventing/sources/sinkbinding/getting-started.md create mode 100644 docs/eventing/sources/sinkbinding/reference.md diff --git a/docs/eventing/sources/sinkbinding.md b/docs/eventing/sources/sinkbinding.md deleted file mode 100644 index 58c4881cf..000000000 --- a/docs/eventing/sources/sinkbinding.md +++ /dev/null @@ -1,244 +0,0 @@ ---- -title: "Sink binding" -weight: 60 -type: "docs" -aliases: - - /docs/eventing/samples/sinkbinding/index - - /docs/eventing/samples/sinkbinding/README ---- - -# Sink binding - -![version](https://img.shields.io/badge/API_Version-v1-red?style=flat-square) - -The `SinkBinding` custom object supports decoupling event production from delivery addressing. - -You can use sink binding to connect Kubernetes resources that embed a `PodSpec` and want to produce events, such as an event source, to an addressable Kubernetes object that can receive events, also known as an _event sink_. - -Sink binding can be used to create new event sources using any of the familiar compute objects that Kubernetes makes available. -For example, `Deployment`, `Job`, `DaemonSet`, or `StatefulSet` objects, or Knative abstractions, such as `Service` or `Configuration` objects, can be used. - -Sink binding injects environment variables into the `PodTemplateSpec` of the event sink, so that the application code does not need to interact directly with the Kubernetes API to locate the event destination. - -Sink binding operates in one of two modes; `Inclusion` or `Exclusion`. -You can set the mode by modifying the `SINK_BINDING_SELECTION_MODE` of the `eventing-webhook` deployment accordingly. The mode determines the default scope of the webhook. - -By default, the webhook is set to `exclusion` mode, which means that any namespace that does not have the label `bindings.knative.dev/exclude: true` will be subject to mutation evalutation. - -If `SINK_BINDING_SELECTION_MODE` is set to `inclusion`, only the resources in a namespace labelled with `bindings.knative.dev/include: true` will be considered. In `inclusion` mode, any SinkBinding resource created will automatically label the `subject` namespace with `bindings.knative.dev/include: true` for inclusion in the potential environment variable inclusions. - -## Getting started - -The following procedures show how you can create a sink binding and connect it to a service and event source in your cluster. - -### Creating a namespace - -Create a namespace called `sinkbinding-example`: - - ```bash - kubectl create namespace sinkbinding-example - ``` - -### Creating a Knative service - -Create a Knative service if you do not have an existing event sink that you want to connect to the sink binding. - -#### Prerequisites -- You must have Knative Serving installed on your cluster. -- Optional: If you want to use `kn` commands with sink binding, you must install the `kn` CLI. - -#### Procedure -Create a Knative service: - - -=== "kn" - - ```bash - kn service create hello --image gcr.io/knative-releases/knative.dev/eventing/cmd/event_display --env RESPONSE="Hello Serverless!" - ``` - - -=== "yaml" - - 1. Copy the sample YAML into a `service.yaml` file: - ```yaml - apiVersion: serving.knative.dev/v1 - kind: Service - metadata: - name: event-display - spec: - template: - spec: - containers: - - image: gcr.io/knative-releases/knative.dev/eventing/cmd/event_display - ``` - 2. Apply the file: - ```bash - kubectl apply --filename service.yaml - ``` - - - - - -### Creating a cron job - -Create a cron job if you do not have an existing event source that you want to connect to the sink binding. - - -Create a `CronJob` object: - -1. Copy the sample YAML into a `cronjob.yaml` file: - ```yaml - apiVersion: batch/v1beta1 - kind: CronJob - metadata: - name: heartbeat-cron - spec: - # Run every minute - schedule: "*/1 * * * *" - jobTemplate: - metadata: - labels: - app: heartbeat-cron - spec: - template: - spec: - restartPolicy: Never - containers: - - name: single-heartbeat - image: gcr.io/knative-releases/knative.dev/eventing/cmd/heartbeats - args: - - --period=1 - env: - - name: ONE_SHOT - value: "true" - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - ``` -2. Apply the file: - ```bash - kubectl apply --filename heartbeats-source.yaml - ``` - -#### Cloning a sample heartbeat cron job - -Knative [event-contrib](https://github.com/knative/eventing) contains a -sample heartbeats event source. - -##### Prerequisites - -- Ensure that `ko publish` is set up correctly: - - [`KO_DOCKER_REPO`](https://github.com/knative/serving/blob/main/DEVELOPMENT.md#environment-setup) - must be set. For example, `gcr.io/[gcloud-project]` or `docker.io/`. - - You must have authenticated with your `KO_DOCKER_REPO`. - -##### Procedure - -1. Clone the `event-contib` repository: - ```bash - $ git clone -b "{{ branch }}" https://github.com/knative/eventing.git - ``` -2. Build a heartbeats image, and publish the image to your image repository: - ```bash - $ ko publish knative.dev/eventing/cmd/heartbeats - ``` - - -## Creating a SinkBinding object - -Create a `SinkBinding` object that directs events from your cron job to the event sink. - -### Prerequisites - -- You must have Knative Eventing installed on your cluster. -- Optional: If you want to use `kn` commands with sink binding, you must install the `kn` CLI. - -### Procedure - -Create a sink binding: - - -=== "kn" - - ```bash - kn source binding create bind-heartbeat \ - --namespace sinkbinding-example \ - --subject "Job:batch/v1:app=heartbeat-cron" \ - --sink http://event-display.svc.cluster.local \ - --ce-override "sink=bound" - ``` - - -=== "yaml" - - 1. Copy the sample YAML into a `cronjob.yaml` file: - ```yaml - apiVersion: sources.knative.dev/v1alpha1 - kind: SinkBinding - metadata: - name: bind-heartbeat - spec: - subject: - apiVersion: batch/v1 - kind: Job - selector: - matchLabels: - app: heartbeat-cron - sink: - ref: - apiVersion: serving.knative.dev/v1 - kind: Service - name: event-display - ``` - 2. Apply the file: - ```bash - kubectl apply --filename heartbeats-source.yaml - ``` - - - - - -## Verification steps - -1. Verify that a message was sent to the Knative eventing system by looking at the `event-display` service logs: - ```bash - kubectl logs -l serving.knative.dev/service=event-display -c user-container --since=10m - ``` -2. Observe the lines showing the request headers and body of the event message, sent by the heartbeats source to the display function: - ```bash - ☁️ cloudevents.Event - Validation: valid - Context Attributes, - specversion: 1.0 - type: dev.knative.eventing.samples.heartbeat - source: https://knative.dev/eventing/cmd/heartbeats/#default/heartbeat-cron-1582120020-75qrz - id: 5f4122be-ac6f-4349-a94f-4bfc6eb3f687 - time: 2020-02-19T13:47:10.41428688Z - datacontenttype: application/json - Extensions, - beats: true - heart: yes - the: 42 - Data, - { - "id": 1, - "label": "" - } - ``` - -## Cleanup - -Delete the `sinkbinding-example` namespace and all of its resources from your -cluster: - - ```bash - kubectl delete namespace sinkbinding-example - ``` diff --git a/docs/eventing/sources/sinkbinding/README.md b/docs/eventing/sources/sinkbinding/README.md new file mode 100644 index 000000000..02b011662 --- /dev/null +++ b/docs/eventing/sources/sinkbinding/README.md @@ -0,0 +1,19 @@ +# SinkBinding + +![API version v1](https://img.shields.io/badge/API_Version-v1-red?style=flat-square) + +The SinkBinding object supports decoupling event production from +delivery addressing. + +You can use sink binding to direct a subject to a sink. +A _subject_ is a Kubernetes resource that embeds a PodSpec template and produces events. +A _sink_ is an addressable Kubernetes object that can receive events. + +The SinkBinding object injects environment variables into the PodTemplateSpec of the +sink. Because of this, the application code does not need to interact +directly with the Kubernetes API to locate the event destination. +These environment variables are as follows: + +- `K_SINK` - The URL of the resolved sink. +- `K_CE_OVERRIDES` - A JSON object that specifies overrides to the outbound + event. diff --git a/docs/eventing/sources/sinkbinding/getting-started.md b/docs/eventing/sources/sinkbinding/getting-started.md new file mode 100644 index 000000000..d1caa1ae9 --- /dev/null +++ b/docs/eventing/sources/sinkbinding/getting-started.md @@ -0,0 +1,294 @@ +# Create a SinkBinding object + +![API version v1](https://img.shields.io/badge/API_Version-v1-red?style=flat-square) + +This topic describes how to create a SinkBinding object. +The SinkBinding resolves a sink as a URI, sets the URI in the environment +variable `K_SINK`, and adds the URI to a subject using `K_SINK`. +If the URI changes, the SinkBinding updates the value of `K_SINK`. + +In the examples below, the sink is a Knative Service and the subject is a CronJob. +If you have an existing subject and sink, you can replace the examples with your +own values. + +## Before you begin + +Before you can create a SinkBinding object: + +- You must have Knative Eventing installed on your cluster. +- Optional: If you want to use `kn` commands with SinkBinding, install the `kn` CLI. + + +## Optional: Choose SinkBinding namespace selection behavior + +The SinkBinding object operates in one of two modes: `exclusion` or `inclusion`. + +The default mode is `exclusion`. +In exclusion mode, SinkBinding behavior is enabled for the namespace by default. +To disallow a namespace from being evaluated for mutation you must exclude it +using the label `bindings.knative.dev/exclude: true`. + +In inclusion mode, SinkBinding behavior is not enabled for the namespace. +Before a namespace can be evaluated for mutation, you must +explicitly include it using the label `bindings.knative.dev/include: true`. + +To set the SinkBinding object to inclusion mode: + +1. Change the value of `SINK_BINDING_SELECTION_MODE` from `exclusion` to `inclusion` by running: + + ```bash + kubectl -n knative-eventing set env deployments eventing-webhook --containers="eventing-webhook" SINK_BINDING_SELECTION_MODE=inclusion + ``` + +2. To verify that `SINK_BINDING_SELECTION_MODE` is set as desired, run: + + ```bash + kubectl -n knative-eventing set env deployments eventing-webhook --containers="eventing-webhook" --list | grep SINK_BINDING + ``` + + +## Create a namespace + +If you do not have an existing namespace, create a namespace for the SinkBinding: + +```bash +kubectl create namespace +``` +Where `` is the namespace that you want your SinkBinding to use. +For example, `sinkbinding-example`. + +!!! note + If you have selected inclusion mode, you must add the + `bindings.knative.dev/include: true` label to the namespace to enable + SinkBinding behavior. + + +## Create a sink + +The sink can be any addressable Kubernetes object that can receive events. + +If you do not have an existing sink that you want to connect to the SinkBinding, +create a Knative service. + +!!! note + To create a Knative service you must have Knative Serving installed on your cluster. + +=== "kn" + + Create a Knative service by running: + + ```bash + kn service create --image + ``` + Where: + - `` is the name of the application. + - `` is the URL of the image container. + + For example: + + ```bash + $ kn service create hello --image gcr.io/knative-releases/knative.dev/eventing-contrib/cmd/event_display + ``` + +=== "YAML" + 1. Create a Knative service by running: + + ```yaml + kubectl apply -f - < + spec: + template: + spec: + containers: + - image: + EOF + ``` + Where: + - `` is the name of the application. For example, `event-display`. + - `` is the URL of the image container. + For example, `gcr.io/knative-releases/knative.dev/eventing-contrib/cmd/event_display` + + +## Create a subject + +The subject must be a PodSpecable resource. +You can use any PodSpecable resource in your cluster, for example: + +- `Deployment` +- `Job` +- `DaemonSet` +- `StatefulSet` +- `Service.serving.knative.dev` + +If you do not have an existing PodSpecable subject that you want to use, you can +use the following sample to create a CronJob object as the subject. +The following CronJob makes a single cloud event that targets `K_SINK` and adds +any extra overrides given by `CE_OVERRIDES`. + +1. Create the CronJob by running: + + ```yaml + kubectl apply -f - < \ + --namespace \ + --subject "" \ + --sink \ + --ce-override "" + ``` + Where: + - `` is the name of the SinkBinding object you want to create. + - `` is the namespace you created for your SinkBinding to use. + - `` is the subject to connect. Examples: + - `Job:batch/v1:app=heartbeat-cron` matches all jobs in namespace with label `app=heartbeat-cron`. + - `Deployment:apps/v1:myapp` matches a deployment called `myapp` in the namespace. + - `Service:serving.knative.dev/v1:hello` matches the service called `hello`. + - `` is the sink to connect. For example `http://event-display.svc.cluster.local`. + - Optional: `` in the form `key=value`. + Cloud Event overrides control the output format and modifications of the event + sent to the sink and are applied before sending the event. + You can provide this flag multiple times. + + For example: + ```bash + $ kn source binding create bind-heartbeat \ + --namespace sinkbinding-example \ + --subject "Job:batch/v1:app=heartbeat-cron" \ + --sink http://event-display.svc.cluster.local \ + --ce-override "sink=bound" + ``` + + + +=== "YAML" + Create a `SinkBinding` object by running: + + ```yaml + kubectl apply -f - < + spec: + subject: + apiVersion: + kind: + selector: + matchLabels: + : + sink: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: + EOF + ``` + Where: + - `` is the name of the SinkBinding object you want to create. For example, `bind-heartbeat`. + - `` is the API version of the subject. For example `batch/v1`. + - `` is the Kind of your subject. For example `Job`. + - `: ` is a map of key-value pairs to select subjects + that have a matching label. For example, `app: heartbeat-cron` selects any subject + with the label `app=heartbeat-cron`. + - `` is the sink to connect. For example `event-display`. + + For more information about the fields you can configure for the SinkBinding + object, see [Sink Binding Reference](reference.md). + + +## Verify the SinkBinding + +1. Verify that a message was sent to the Knative eventing system by looking at the +service logs for your sink: + + ```bash + kubectl logs -l -c --since=10m + ``` + Where: + - `` is the name of your sink. + - `` is the name of the container your sink is running in. + + For example: + ```bash + $ kubectl logs -l serving.knative.dev/service=event-display -c user-container --since=10m + ``` +2. From the output, observe the lines showing the request headers and body of the event message, +sent by the source to the display function. For example: + + ```bash + ☁️ cloudevents.Event + Validation: valid + Context Attributes, + specversion: 1.0 + type: dev.knative.eventing.samples.heartbeat + source: https://knative.dev/eventing-contrib/cmd/heartbeats/#default/heartbeat-cron-1582120020-75qrz + id: 5f4122be-ac6f-4349-a94f-4bfc6eb3f687 + time: 2020-02-19T13:47:10.41428688Z + datacontenttype: application/json + Extensions, + beats: true + heart: yes + the: 42 + Data, + { + "id": 1, + "label": "" + } + ``` + + +## Cleanup + +To delete the SinkBinding and all of the related resources in the namespace, +delete the namespace by running: + +```shell +kubectl delete namespace +``` +Where `` is the name of the namespace that contains the SinkBinding object. diff --git a/docs/eventing/sources/sinkbinding/reference.md b/docs/eventing/sources/sinkbinding/reference.md new file mode 100644 index 000000000..ee29e154e --- /dev/null +++ b/docs/eventing/sources/sinkbinding/reference.md @@ -0,0 +1,171 @@ +# SinkBinding Reference + +This topic provides reference information about the configurable fields for the +SinkBinding object. + + +## SinkBinding + +A `SinkBinding` definition supports the following fields: + +| Field | Description | Required or optional | +|-------|-------------|----------------------| +| [`apiVersion`][kubernetes-overview] | Specifies the API version, for example `sources.knative.dev/v1`. | Required | +| [`kind`][kubernetes-overview] | Identifies this resource object as a `SinkBinding` object. | Required | +| [`metadata`][kubernetes-overview] | Specifies metadata that uniquely identifies the `SinkBinding` object. For example, a `name`. | Required | +| [`spec`][kubernetes-overview] | Specifies the configuration information for this `SinkBinding` object. | Required | +| [`spec.sink`](#sink-parameter) | A reference to an object that resolves to a URI to use as the sink. | Required | +| [`spec.subject`](#subject-parameter) | A reference to the resources for which the "runtime contract" is augmented by Binding implementations. | Required | +| [`spec.ceOverrides`](#cloudevent-overrides) | Defines overrides to control the output format and modifications to the event sent to the sink. | Optional | + + +### Sink parameter + +Sink is a reference to an object that resolves to a URI to use as the sink. + +A `sink` definition supports the following fields: + +| Field | Description | Required or optional | +|-------|-------------|----------------------| +| `ref` | This points to an Addressable. | Required if _not_ using `uri` | +| `ref.apiVersion` | API version of the referent. | Required if using `ref` | +| [`ref.kind`][kubernetes-kinds] | Kind of the referent. | Required if using `ref` | +| [`ref.namespace`][kubernetes-namespaces] | Namespace of the referent. If omitted this defaults to the object holding it. | Optional | +| [`ref.name`][kubernetes-names] | Name of the referent. | Required if using `ref` | +| `uri` | This can be an absolute URL with a non-empty scheme and non-empty host that points to the target or a relative URI. Relative URIs are resolved using the base URI retrieved from Ref. | Required if _not_ using `ref` | + +**Note:** At least one of `ref` or `uri` is required. If both are specified, `uri` is +resolved into the URL from the Addressable `ref` result. + +#### Example: Sink parameter + +Given the following YAML, if `ref` resolves into +`"http://mysink.default.svc.cluster.local"`, then `uri` is added to this +resulting in `"http://mysink.default.svc.cluster.local/extra/path"`. + + + +```yaml +sink: + ref: + apiVersion: v1 + kind: Service + namespace: default + name: mysink + uri: /extra/path +``` + +**Contract:** This results in the `K_SINK` environment variable being set on the +`subject` as `"http://mysink.default.svc.cluster.local/extra/path"`. + + +### Subject parameter + +The Subject parameter references the resources for which the "runtime contract" +is augmented by Binding implementations. + +A `subject` definition supports the following fields: + +| Field | Description | Required or optional | +|-------|-------------|----------------------| +| `apiVersion` | API version of the referent. | Required | +| [`kind`][kubernetes-kinds] | Kind of the referent. | Required | +| [`namespace`][kubernetes-namespaces] | Namespace of the referent. If omitted, this defaults to the object holding it. | Optional | +| [`name`][kubernetes-names] | Name of the referent. | Do not use if you configure `selector`. | +| `selector` | Selector of the referents. | Do not use if you configure `name`. | +| `selector.matchExpressions` | A list of label selector requirements. The requirements are ANDed. | Use one of `matchExpressions` or `matchLabels` | +| `selector.matchExpressions.key` | The label key that the selector applies to. | Required if using `matchExpressions` | +| `selector.matchExpressions.operator` | Represents a key's relationship to a set of values. Valid operators are `In`, `NotIn`, `Exists` and `DoesNotExist`. | Required if using `matchExpressions` | +| selector.matchExpressions.values` | An array of string values. If `operator` is `In` or `NotIn`, the values array must be non-empty. If `operator` is `Exists` or `DoesNotExist`, the values array must be empty. This array is replaced during a strategic merge patch. | Required if using `matchExpressions` | +| `selector.matchLabels` | A map of key-value pairs. Each key-value pair in the `matchLabels` map is equivalent to an element of `matchExpressions`, where the key field is `matchLabels.`, the `operator` is `In`, and the `values` array contains only "matchLabels.". The requirements are ANDed. | Use one of `matchExpressions` or `matchLabels` | + +#### Example: Subject parameter using name + +Given the following YAML, the `Deployment` named `mysubject` in the `default` +namespace is selected: + + ```yaml + subject: + apiVersion: apps/v1 + kind: Deployment + namespace: default + name: mysubject + ``` + +#### Example: Subject parameter using matchLabels + +Given the following YAML, any `Job` with the label `working=example` in the +`default` namespace is selected: + + ```yaml + subject: + apiVersion: batch/v1beta1 + kind: Job + namespace: default + selector: + matchLabels: + working: example + ``` + +#### Example: Subject parameter using matchExpression + +Given the following YAML, any `Pod` with the label `working=example` OR +`working=sample` in the ` default` namespace is selected: + + ```yaml + subject: + apiVersion: v1 + kind: Pod + namespace: default + selector: + - matchExpression: + key: working + operator: In + values: + - example + - sample + ``` + + +### CloudEvent Overrides + +CloudEvent Overrides defines overrides to control the output format and +modifications of the event sent to the sink. + +A `ceOverrides` definition supports the following fields: + +| Field | Description | Required or optional | +|-------|-------------|----------------------| +| `extensions` | Specifies which attributes are added or overridden on the outbound event. Each `extensions` key-value pair is set independently on the event as an attribute extension. | Optional | + +**Note:** Only valid [CloudEvent attribute names][cloudevents-attribute-naming] +are allowed as extensions. You cannot set the spec defined attributes from +the extensions override configuration. For example, you can not modify the `type` +attribute. + +#### Example: CloudEvent Overrides + +```yaml +ceOverrides: + extensions: + extra: this is an extra attribute + additional: 42 +``` + +**Contract:** This results in the `K_CE_OVERRIDES` environment variable being set on the +`subject` as follows: + +```json +{ "extensions": { "extra": "this is an extra attribute", "additional": "42" } } +``` + +[kubernetes-overview]: + https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields +[kubernetes-kinds]: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds +[kubernetes-names]: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +[kubernetes-namespaces]: + https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ +[cloudevents-attribute-naming]: + https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#attribute-naming-convention diff --git a/mkdocs.yml b/mkdocs.yml index bf10cb134..9732b6c1f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,7 +139,10 @@ nav: - Getting started: eventing/sources/apiserversource/getting-started/README.md - ContainerSource: eventing/sources/containersource.md - PingSource: eventing/sources/ping-source/index.md - - Sink binding: eventing/sources/sinkbinding.md + - SinkBinding: + - Overview: eventing/sources/sinkbinding/README.md + - Create a SinkBinding object: eventing/sources/sinkbinding/getting-started.md + - SinkBinding Reference: eventing/sources/sinkbinding/reference.md - Camel source: eventing/sources/apache-camel-source/README.md - Creating an event source: - Overview: eventing/sources/creating-event-sources/README.md @@ -275,6 +278,8 @@ plugins: strict: false - redirects: redirect_maps: + 'eventing/samples/sinkbinding/': 'eventing/sources/sinkbinding/README.md' + 'eventing/sources/sinkbinding': 'eventing/sources/sinkbinding/README.md' 'install/collecting-logs/index.md': 'admin/collecting-logs/README.md' 'install/README.md': 'admin/install/README.md' 'install/collecting-metrics/index.md': 'admin/collecting-metrics/README.md'