Introduction to API Usage

Authentication/Authorization

The reference implementation of the Results API expects to obtain cluster-generated authentication tokens from the cluster in which it runs. In most cases, using a service account will be the simplest way to interact with the API.

RBAC authorization is used to control access to API resources.

The following attributes are recognized:

AttributeValue
apiGroupsresults.tekton.dev
resourcesresults, records
verbscreate, get, list, update, delete

For example, a read-only role may look as follows:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tekton-results-readonly
rules:
  - apiGroups: ["results.tekton.dev"]
    resources: ["results", "records"]
    verbs: ["get", "list"]

In the reference implementation, all permissions are namespace-scoped (this is what serves as the parent resource for the API).

For convenience, the following [ClusterRoles] define common access patterns:

ClusterRoleDescription
tekton-results-readonlyRead-only access to all Results API resources
tekton-results-readwriteIncludes tekton-results-readonly + create or update all Results API resources
tekton-results-adminIncludes tekton-results-readwrite + permission to delete Results API resources

Impersonation

Kubernetes impersonation is used to refine access to the API.

How to Use Impersonation?

  • Set the feature flag AUTH_IMPERSONATE to true in the API server configuration.

  • Create two ServiceAccounts, one for managing impersonation permissions and the other for the permissions of the impersonated user.

    kubectl create serviceaccount impersonate-admin -n tekton-pipelines
    
    kubectl create serviceaccount impersonate-user -n user-namespace
    
  • Create the following ClusterRole and ClusterRoleBinding for the impersonate-admin service account.

    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: tekton-results-impersonate
    rules:
      - apiGroups: [""]
        resources: ["users", "groups", "serviceaccounts"]
        verbs: ["impersonate"]
      - apiGroups: ["authentication.k8s.io"]
        resources: ["uids"]
        verbs: ["impersonate"]
    
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: tekton-results-impersonate
    subjects:
      - kind: ServiceAccount
        name: impersonate-admin
        namespace: tekton-pipelines
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: tekton-results-impersonate
    
  • Finally, create a RoleBinding for the impersonate-user service account.

    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: tekton-results-user
      namespace: user-namespace
    subjects:
      - kind: ServiceAccount
        name: impersonate-user
        namespace: user-namespace
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: tekton-results-readonly
    
  • Now retrieve the token for the impersonate-admin service account and store it in a variable.

    token=$(kubectl create token impersonate-admin -n tekton-pipelines)
    
  • You can then call the API in the following format:

    curl -s --cacert /var/tmp/tekton/ssl/tls.crt  \
      -H 'authorization: Bearer '${token} \
      -H 'Impersonate-User: system:serviceaccount:user-namespace:impersonate-user' \
      https://localhost:8080/apis/results.tekton.dev/v1alpha2/parents/user-namespace/results
    

If the API server uses TLS, you will need to provide the TLS certificate.

Troubleshooting

You can run the following command to query the permissions of the cluster. This is very useful for debugging permission denied errors:

kubectl create --as=system:serviceaccount:tekton-pipelines:tekton-results-watcher -n tekton-pipelines -f - -o yaml << EOF
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
spec:
  resourceAttributes:
    group: results.tekton.dev
    resource: results
    verb: get
EOF
apiVersion: authorization.k8s.io/v1
kind: SelfSubjectAccessReview
metadata:
  creationTimestamp: null
  managedFields:
  - apiVersion: authorization.k8s.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:resourceAttributes:
          .: {}
          f:group: {}
          f:resource: {}
          f:verb: {}
    manager: kubectl
    operation: Update
    time: "2021-02-02T22:37:32Z"
spec:
  resourceAttributes:
    group: results.tekton.dev
    resource: results
    verb: get
status:
  allowed: true
  reason: 'RBAC: allowed by ClusterRoleBinding "tekton-results-watcher" of ClusterRole
    "tekton-results-watcher" to ServiceAccount "tekton-results-watcher/tekton-pipelines"'

Filtering

The reference implementation of the Results API uses CEL as its filtering specification. The filtering specification expects a boolean Results value. This document covers a small portion of CEL that is useful for filtering Results and records.

Results

The following is a mapping between Results JSON/protobuf fields and CEL references:

FieldCEL Reference FieldDescription
-parentThe name of the parent (workspace/namespace) of Results.
uiduidThe unique identifier of Results.
annotationsannotationsAnnotations added to Results.
summarysummaryThe summary of Results.
createTimecreate_timeThe creation time of Results.
updateTimeupdate_timeThe last update time of Results.

The summary.status field is an enumeration and must be used without quotes in filtering expressions (' or "). Possible values are:

  • UNKNOWN
  • SUCCESS
  • FAILURE
  • TIMEOUT
  • CANCELLED

Records and Logs

The following is a mapping between record JSON/protobuf fields and CEL references:

FieldCEL Reference FieldDescription
namenameThe name of the record
data.typedata_typeThe type identifier of record data. Please see the values below.
data.valuedataThe data contained in the record. In JSON and protobuf responses, it appears as a base 64 encoded string.

Possible values for data_type and summary.type (for Results) are:

  • tekton.dev/v1beta1.TaskRun or TASK_RUN
  • tekton.dev/v1beta1.PipelineRun or PIPELINE_RUN
  • results.tekton.dev/v1alpha2.Log

Thedata Field in Records

The data field is a base64 encoded string of an object manifest. If you directly request this data using CLI, REST, or gRPC, you will receive a base64 encoded string. You can decode it using the base64 -d command. While it's not human-readable, you can directly filter the response using filters without decoding it.

Here is an example of a JSON object contained in the data field of records. This can be directly mapped to the YAML representation we typically use.

{
  "kind": "PipelineRun",
  "spec": {
    "timeout": "1h0m0s",
    "pipelineSpec": {
      "tasks": [
        {
          "name": "hello",
          "taskSpec": {
            "spec": null,
            "steps": [
              {
                "name": "hello",
                "image": "ubuntu",
                "script": "echo hello world!",
                "resources": {}
              }
            ],
            "metadata": {}
          }
        }
      ]
    },
    "serviceAccountName": "default"
  },
  "status": {
    "startTime": "2023-08-22T09:08:59Z",
    "conditions": [
      {
        "type": "Succeeded",
        "reason": "Succeeded",
        "status": "True",
        "message": "Tasks Completed: 1 (Failed: 0, Cancelled 0), Skipped: 0",
        "lastTransitionTime": "2023-08-22T09:09:31Z"
      }
    ],
    "pipelineSpec": {
      "tasks": [
        {
          "name": "hello",
          "taskSpec": {
            "spec": null,
            "steps": [
              {
                "name": "hello",
                "image": "ubuntu",
                "script": "echo hello world!",
                "resources": {}
              }
            ],
            "metadata": {}
          }
        }
      ]
    },
    "completionTime": "2023-08-22T09:09:31Z",
    "childReferences": [
      {
        "kind": "TaskRun",
        "name": "hello-hello",
        "apiVersion": "tekton.dev/v1beta1",
        "pipelineTaskName": "hello"
      }
    ]
  },
  "metadata": {
    "uid": "1638b693-844d-4f13-b767-d7d84ac4ab3d",
    "name": "hello",
    "labels": {
      "tekton.dev/pipeline": "hello"
    },
    "namespace": "default",
    "generation": 1,
    "annotations": {
      "results.tekton.dev/record": "default/results/1638b693-844d-4f13-b767-d7d84ac4ab3d/records/1638b693-844d-4f13-b767-d7d84ac4ab3d",
      "results.tekton.dev/result": "default/results/1638b693-844d-4f13-b767-d7d84ac4ab3d",
      "results.tekton.dev/resultAnnotations": "{\"repo\": \"tektoncd/results\", \"commit\": \"1a6b908\"}",
      "results.tekton.dev/recordSummaryAnnotations": "{\"foo\": \"bar\"}",
      "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"tekton.dev/v1beta1\",\"kind\":\"PipelineRun\",\"metadata\":{\"annotations\":{\"results.tekton.dev/recordSummaryAnnotations\":\"{\\\"foo\\\": \\\"bar\\\"}\",\"results.tekton.dev/resultAnnotations\":\"{\\\"repo\\\": \\\"tektoncd/results\\\", \\\"commit\\\": \\\"1a6b908\\\"}\"},\"name\":\"hello\",\"namespace\":\"default\"},\"spec\":{\"pipelineSpec\":{\"tasks\":[{\"name\":\"hello\",\"taskSpec\":{\"steps\":[{\"image\":\"ubuntu\",\"name\":\"hello\",\"script\":\"echo hello world!\"}]}}]}}}\n"
    },
    "managedFields": [
      {
        "time": "2023-08-22T09:08:59Z",
        "manager": "controller",
        "fieldsV1": {
          "f:metadata": {
            "f:labels": {
              ".": {},
              "f:tekton.dev/pipeline": {}
            }
          }
        },
        "operation": "Update",
        "apiVersion": "tekton.dev/v1beta1",
        "fieldsType": "FieldsV1"
      },
      {
        "time": "2023-08-22T09:08:59Z",
        "manager": "kubectl-client-side-apply",
        "fieldsV1": {
          "f:spec": {
            ".": {},
            "f:pipelineSpec": {
              ".": {},
              "f:tasks": {}
            }
          },
          "f:metadata": {
            "f:annotations": {
              ".": {},
              "f:results.tekton.dev/resultAnnotations": {},
              "f:results.tekton.dev/recordSummaryAnnotations": {},
              "f:kubectl.kubernetes.io/last-applied-configuration": {}
            }
          }
        },
        "operation": "Update",
        "apiVersion": "tekton.dev/v1beta1",
        "fieldsType": "FieldsV1"
      },
      {
        "time": "2023-08-22T09:08:59Z",
        "manager": "watcher",
        "fieldsV1": {
          "f:metadata": {
            "f:annotations": {
              "f:results.tekton.dev/record": {},
              "f:results.tekton.dev/result": {}
            }
          }
        },
        "operation": "Update",
        "apiVersion": "tekton.dev/v1beta1",
        "fieldsType": "FieldsV1"
      },
      {
        "time": "2023-08-22T09:09:31Z",
        "manager": "controller",
        "fieldsV1": {
          "f:status": {
            ".": {},
            "f:startTime": {},
            "f:conditions": {},
            "f:pipelineSpec": {
              ".": {},
              "f:tasks": {}
            },
            "f:completionTime": {},
            "f:childReferences": {}
          }
        },
        "operation": "Update",
        "apiVersion": "tekton.dev/v1beta1",
        "fieldsType": "FieldsV1",
        "subresource": "status"
      }
    ],
    "resourceVersion": "1567",
    "creationTimestamp": "2023-08-22T09:08:59Z"
  },
  "apiVersion": "tekton.dev/v1beta1"
}

You can now access the required fields using dot notation and treat data as the parent object. For example:

PurposeFiltering Expression
The name of the PipelineRundata.metadata.name
The name of the ServiceAccount used in the PipelineRundata.spec.serviceAccountName
The name of the first task in the PipelineRundata.spec.pipelineSpec.tasks[0].name
The start time of the PipelineRundata.status.startTime
The labels of the PipelineRundata.metadata.labels
The annotations of the PipelineRundata.metadata.annotations
A specific annotation of the PipelineRundata.metadata.annotations['foo']

Thedata Field in Logs

The data field is a base64 encoded custom object of type Log. Accessing fields is similar to records. Here is an example of a JSON object contained in the data field of logs.

{
  "kind": "Log",
  "spec": {
    "type": "File",
    "resource": {
      "uid": "dbe14a60-1fc8-458e-a49b-264771557c3e",
      "kind": "TaskRun",
      "name": "hello",
      "namespace": "default"
    }
  },
  "status": {
    "path": "default/2d27dde5-e201-35f9-b658-2456f2955903/hello-log",
    "size": 83
  },
  "metadata": {
    "uid": "2d27dde5-e201-35f9-b658-2456f2955903",
    "name": "hello-log",
    "namespace": "default",
    "creationTimestamp": null
  },
  "apiVersion": "results.tekton.dev/v1alpha2"
}

Here are some examples of accessing log fields using dot notation:

PurposeFiltering Expression
The type of running that created this logdata.spec.resource.kind
The size of the logdata.status.size
The name of the running that created this logdata.spec.resource.name

How to Create CEL Filtering Expressions

CEL expressions consist of identifiers, literals, operators, and functions. Here, we will learn how to create CEL filtering expressions using the fields mentioned above. This is not an exhaustive list of CEL expressions. For more information, please refer to the CEL specification.

Accessing Fields

CEL expressions typically correspond one-to-one with JSON/protobuf fields. In Tekton Results, we created some additional aliases for convenient access. You can see all of these in the table above. Additionally, you can use dot notation to access any field in the JSON/protobuf objects. See the examples in the table below.

PurposeFiltering ExpressionDescription
The status of Resultssummary.statusThe status of Results is a sub-object of summary.
The data type of the recorddata_typedata_type is an alias for the data.type of the record.
The data type of Resultssummary.typeThe type of Results is a sub-object of summary.
The name of the first step of the running task retrieved from statusdata.status.taskSpec.steps[0].nameThe JSON path is data -> status -> taskSpec -> steps -> 0 -> name.

Using Operators

Now that we can access fields, you can create filters using operators. Here’s a list of operators that can be used in CEL expressions:

OperatorDescriptionExample
==Equalitydata_type == "tekton.dev/v1beta1.TaskRun"
!=Inequalitysummary.status != SUCCESS
INIn listdata.metadata.name in ['hello', 'foo', 'bar']
!Negation!(data.status.name in ['hello', 'foo', 'bar'])
&&Logical anddata_type == "tekton.dev/v1beta1.TaskRun" && name.startsWith("foo/results/bar")
||Logical ordata_type == "tekton.dev/v1beta1.TaskRun" || data_type == "tekton.dev/v1beta1.PipelineRun"
+, -, *, /, %Arithmetic operatorsdata.status.completionTime - data.status.startTime > duration('5m')
>, >=, <, <=Comparison operatorsdata.status.completionTime > data.status.startTime

Using Functions

Many functions can be used in CEL expressions. Here’s a list of functions that can be used in CEL expressions. The strings in function parameters represent the expected type of the arguments:

FunctionDescriptionExample
startsWith('string')Check if a string starts with a certain prefixdata.metadata.name.startsWith("foo")
endsWith('string')Check if a string ends with a certain suffixdata.metadata.name.endsWith("bar")
contains('string')Check if a field exists or an object contains a key or valuedata.metadata.annotations.contains('bar')
timestamp('RFC3339-timestamp')For comparing timestampsdata.status.startTime > timestamp("2021-02-02T22:37:32Z")
getDate()Returns the date from a timestampdata.status.completionTime.getDate() == 7
getDayOfWeek(), getDayOfMonth(), getDayOfYear()Returns the day of the week, day of the month, or day of the year from a timestampdata.status.completionTime.getDayOfMonth() == 7
getFullYear()Returns the year from a timestampdata.status.startTime.getFullYear() == 2023
getHours(), getMinutes(), getSeconds()Returns hours, minutes, or seconds from a timestampdata.status.completionTime.getHours() >= 9
string('input')Converts valid input to a stringstring(data.status.completionTime) == "2021-02-02T22:37:32Z"
matches('regex')Checks if a string matches a regular expressionname.matches("^foo.*$")

You can also nest function calls and mix operators to create complex filtering expressions. Make sure to use the correct types for function arguments. You can see a more in-depth function reference in the CEL specification. The functions mentioned above are the most commonly used and effective.

Using CEL Filtering Expressions with gRPC

You can pass a filter to gRPC requests by specifying filter=<cel-expression>. Be sure to use the correct quoting in your queries or escape if necessary. Here’s an example:

grpc_cli call --channel_creds_type=ssl \
  --ssl_target=tekton-results-api-service.tekton-pipelines.svc.cluster.local \
  --call_creds=access_token=$ACCESS_TOKEN \
  localhost:8080 tekton.results.v1alpha2.Results.ListResults \
  'parent:"default",filter:"data_type==TASK_RUN"'

Using CEL Filtering Expressions with REST

You can pass a filter to REST requests by specifying filter=<cel-expression> in your query. Here’s an example:

curl --insecure \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json" \
  https://localhost:8080/apis/results.tekton.dev/v1alpha2/parents/-/results/-?filter=data.status.completionTime.getDate()==7

Using CEL Filtering Expressions withtkn-results

If you have installed tkn-results CLI independently or as a plugin to tkn, you can filter Results using the --filter=<cel-expression> flag. Here’s an example:

tkn results records list default/results/- --filter="data.metadata.annotations.contains('bar')"

Common Filtering Examples

These examples showcase the most common filtering expressions that are very useful for everyday use. Remember that not all of these filters apply to Results, records, and logs. You must provide the correct filter for the correct resource.

PurposeFiltering Expression
Get all records where TaskRun/PipelineRun name is hellodata.metadata.name == 'hello'
Get all records of TaskRun belonging to PipelineRun 'foo'data.metadata.labels['tekton.dev/pipelineRun'] == 'foo'
Get all records of TaskRun/PipelineRun belonging to Pipeline 'bar'data.metadata.labels['tekton.dev/pipeline'] == 'bar'
The same as the above query, but I only want PipelineRundata.metadata.labels['tekton.dev/pipeline'] == 'bar' && data_type == 'PIPELINE_RUN'
Get records of TaskRun whose names start with hellodata.metadata.name.startsWith('hello')&&dat_type==TASK_RUN
Get Results of all successful TaskRunssummary.status == SUCCESS && summary.type == 'TASK_RUN'
Get records of PipelineRun whose completion time exceeds 5 minutesdata.status.completionTime - data.status.startTime > duration('5m') && data_type == 'PIPELINE_RUN'
Get records of runs completed today (assuming today is the 7th)data.status.completionTime.getDate() == 7
Get records of PipelineRun that have annotation containing bardata.metadata.annotations.contains('bar') && data_type == 'PIPELINE_RUN'
Get records of PipelineRun with an annotation containing bar and whose names start with foodata.metadata.annotations.contains('bar') && data.metadata.name.startsWith('foo') && data_type == 'PIPELINE_RUN'
Get Results containing annotations foo and barsummary.annotations.contains('foo') && summary.annotations.contains('bar')
Get Results of all failed runs!(summary.status == SUCCESS)
Get records of all failed runs!(data.status.conditions[0].status == 'True')
Get all records of PipelineRun with 3 or more taskssize(data.status.pipelineSpec.tasks) >= 3 && data_type == 'PIPELINE_RUN'

Sorting

The reference implementation of the Results API supports sorting Results and records responses using optional direction qualifiers (asc or desc).

To request a list of objects in a specific order, include the order_by query parameter in the request. Pass the name of the field to be sorted. Multiple fields can be specified using a comma-separated list. Examples:

  • create_time
  • update_time asc
  • create_time desc, update_time asc

Fields supported in order_by:

Field Name
create_time
update_time

Pagination

The reference implementation of the Results API supports pagination for Results, records, and logs. The default number of objects in a single page is 50, with a maximum of 10,000.

To paginate responses, include the page_size query parameter in the request. It must be an integer value between 0 and 10,000. If page_size is less than the total number of objects available for a specific query, the response will contain a NextPageToken. You can pass this value to the page_token query parameter to retrieve the next page. These two queries are independent and can be used separately or together.

NameDescription
page_sizeThe number of objects to retrieve in the response.
page_tokenThe token for the page to be retrieved.

Cross-parent Reading Results

You can read Results across parents by specifying - as the parent name. This is useful for listing all Results stored in the system without prior knowledge of the available parents.

Cross-Results Reading Records

You can read records across Results by specifying - as the Result name portion, or - as the parent name. (For example, default/results/- or -/results/-). This can be used to read and filter matching records when the exact Result name is unknown.

Metrics

The API server includes an HTTP server to expose gRPC server Prometheus metrics. By default, metrics are exposed on port :9090. For more detailed information about the metrics structure, see https://github.com/grpc-ecosystem/go-grpc-prometheus#metrics.

Health

The API server includes gRPC and REST endpoints to monitor the service status of the API server and the individual services.

Check Status

# Check the status of the API server using gRPC
grpcurl --insecure localhost:8080 grpc.health.v1.Health/Check

# Check the status of an individual service using gRPC
grpcurl --insecure -d '{"service": "tekton.results.v1alpha2.Results"}' localhost:8080 grpc.health.v1.Health/Check

# Check the status of the API server using REST
curl -k https://localhost:8080/healthz

# Check the status of an individual service using REST
curl -k https://localhost:8080/healthz?service=tekton.results.v1alpha2.Results