Skip to content

Route directive adapter development guide

Kuat edited this page Nov 21, 2018 · 13 revisions

Warning: Mixer route directives are released as an alpha feature in istio 1.1 and may change in the future

This guide should take less than 45 minutes to follow.

Route directives enable Mixer adapters to modify traffic metadata using operation templates on the request and response headers. In this guide, we develop a simple functional out-of-process check adapter that produces extra output in addition to the check result. This adapter matches a key in the request headers against an external table of users and keys, and outputs the username on successful authorization checks.

You should be familiar with the core Istio concepts such as handlers, rules, and gateways, before attempting this guide. You should also have a development environment available, with an access to a docker registry.

Step 0: Deploy ingress sample

In this guide, we are going to use httpbin sample application. Follow the ingress task to install Istio into istio-system namespace, and deploy httpbin application into the default namespace.

Make sure you have /headers exposed in the virtual service routes. You should have the following networking configuration applied:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  gateways:
  - httpbin-gateway
  hosts:
  - httpbin.example.com
  http:
  - match:
    - uri:
        prefix: /status
    - uri:
        prefix: /delay
    - uri:
        prefix: /headers
    route:
    - destination:
        host: httpbin
        port:
          number: 8000
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - hosts:
    - httpbin.example.com
    port:
      name: http
      number: 80
      protocol: HTTP

You can validate that the installation is successful with the following command:

curl -H Host:httpbin.example.com -v http://${INGRESS}/headers

where ${INGRESS} is the address of the ingressgateway service (see the ingress traffic management task). The command should return both the request headers as seen by httpbin application in the cluster, as well as the response headers as seen by curl client.

You also need a development environment set-up with Go and Kubernetes per developer Guide.

Download and build a local copy of Istio:

mkdir -p $GOPATH/src/istio.io/
cd $GOPATH/src/istio.io/ 
git clone https://github.com/istio/istio
cd istio
go build ./...

Note: check out release-1.1 branch before 1.1 is released using git checkout release-1.1

Install protoc (version 3.5.1 or higher) from https://github.com/google/protobuf/releases and make it available as an executable from $PATH.

Step 1: Define a template

First, let us define a new template that takes an instance with a request key and a request path, and produces an instance with the user name. Save the following template proto definition as a file userkey/template.proto under Istio root directory:

syntax = "proto3";
package userkey;
import "mixer/adapter/model/v1beta1/extensions.proto";
option (istio.mixer.adapter.model.v1beta1.template_variety) = TEMPLATE_VARIETY_CHECK_WITH_OUTPUT;
message Template {
  string key = 1;
  string path = 2;
}
message OutputTemplate {
  string user = 1;
}

Run the following command under Istio root to generate the code stubs for the new template:

bin/mixer_codegen.sh -t userkey/template.proto

Step 2: Implement the adapter

To implement an adapter for the new template, we first need to define its configuration. Create file userkey/config/config.proto with the following content:

syntax = "proto3";
import "google/protobuf/duration.proto";
import "gogoproto/gogo.proto";
package config;
message Params {
  google.protobuf.Duration valid_duration = 1 [(gogoproto.nullable)=false, (gogoproto.stdduration) = true];
}

This adapter config consists of a single parameter that defines the validity duration of the successful check results.

Run the following command to generate the adapter definition:

bin/mixer_codegen.sh -a userkey/config/config.proto -x "-s=false -n userkey -t userkey"

This command produces a session-less adapter called userkey that implements the template userkey.

We are now ready to implement the adapter. Save the following implementation as userkey/userkey.go:

package userkey

import (
        "github.com/gogo/googleapis/google/rpc"
        "golang.org/x/net/context"

        "istio.io/api/mixer/adapter/model/v1beta1"
        "istio.io/istio/userkey/config"
)

type Userkey struct{}

var externalTable = map[string]string{"foobar": "jason"}

func (Userkey) HandleUserkey(_ context.Context, req *HandleUserkeyRequest) (*HandleUserkeyResponse, error) {
        config := &config.Params{}
        if err := config.Unmarshal(req.AdapterConfig.Value); err != nil {
                return nil, err
        }

        user, ok := externalTable[req.Instance.Key]
        if ok {
                return &HandleUserkeyResponse{
                        Result: &v1beta1.CheckResult{ValidDuration: config.ValidDuration},
                        Output: &OutputMsg{User: user},
                }, nil
        }

        return &HandleUserkeyResponse{
                Result: &v1beta1.CheckResult{
                        Status: rpc.Status{Code: int32(rpc.PERMISSION_DENIED)},
                },
        }, nil
}

The code above takes a key from the input instance and queries a table of users. If a match is found, it returns a successful check result together with the username from the table using the configured validity duration. Otherwise, it returns a permission denied error code.

We also need to define a main file for the adapter. Save the following main function as userkey/main/main.go:

package main

import (
        "net"

        "google.golang.org/grpc"
        "istio.io/istio/userkey"
)

func main() {
        listener, err := net.Listen("tcp", ":9070")
        if err != nil {
                panic(err)
        }
        server := grpc.NewServer()
        userkey.RegisterHandleUserkeyServiceServer(server, userkey.Userkey{})
        server.Serve(listener)
}

Check that the adapter runs successfully locally first:

go run userkey/main/main.go

If all is good, you should see no errors and a local adapter server listening on port 9070.

Step 3: Deploy the template and the adapter

We are now ready to deploy our new adapter to the remote istio installation. First, we need to package it as a docker container. For this, you need to have write access to a docker registry. Export the image name ($HUB:$TAG combination) as ${DOCKER_IMAGE} environment variable.

  1. Run the following command to generate a Linux static binary:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o userkey/userkey ./userkey/main/main.go
  1. Create a docker image:
cat <<EOF > userkey/Dockerfile
FROM scratch
ADD userkey /userkey
EXPOSE 9070
ENTRYPOINT ["/userkey"]
EOF
docker build -t ${DOCKER_IMAGE} userkey/
docker push ${DOCKER_IMAGE}
  1. Run the adapter container image in the cluster:
kubectl run userkey --image=${DOCKER_IMAGE} --namespace istio-system --port 9070 --expose

Note: you can use the image gcr.io/istio-testing/userkey in place of DOCKER_IMAGE in the last step if you encounter issues in the previous steps.

The last command creates a new pod running the adapter in istio-system namespace and a service definition for it exposing the adapter interface on port 9070.

  1. Deploy template and adapter definitions to Mixer.

Run the following commands to publish template and adapter configurations:

kubectl apply -f userkey/template.yaml
kubectl apply -f userkey/config/userkey.yaml

Step 4: Connect Mixer to the adapter

  1. Let us instantiate the adapter by adding a handler to Mixer:
cat <<EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  name: h1
  namespace: istio-system
spec:
  adapter: userkey
  connection:
    address: userkey:9070
  params:
    valid_duration: 1s
EOF

Note the address is the service userkey created in the previous step.

  1. Next, we provide an input instance that uses a request header named key to fill in the value:
cat <<EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: instance
metadata:
  name: i1
  namespace: istio-system
spec:
  template: userkey
  params:
    key: request.headers["key"] | "unknown"
EOF
  1. Finally, we add a rule to invoke the handler on the instance:
cat <<EOF | kubectl create -f -
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: r1
  namespace: istio-system
spec:
  actions:
  - handler: h1.istio-system
    instances: ["i1"]
EOF

To make sure everything is working correctly, let us access the application through the gateway. First, make a request without the key header:

curl -H Host:httpbin.example.com -v http://${INGRESS}/headers

You should see the following response:

PERMISSION_DENIED:h1.handler.istio-system

Now make the same request but with the header supplied:

curl -H Host:httpbin.example.com -H key:foobar -v http://${INGRESS}/headers

You should see a successful response:

> GET /headers HTTP/1.1
...
< HTTP/1.1 200 OK
...
{
  "headers": {
    "Accept": "*/*",
    "Content-Length": "0",
    "Host": "httpbin.example.com",
    "Key": "foobar",
    "User-Agent": "curl/7.58.0",
    ...
  }
}

Step 5: Try request header operations

Let us modify the rule to send a request header to the back-end application. Using kubectl edit rule/r1 -n istio-system change it to be as follows:

apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: r1
  namespace: istio-system
spec:
  # restrict the rule to the ingress gateway proxy workload only
  match: context.reporter.kind == "outbound" && source.labels["istio"] == "ingressgateway"
  actions:
  - handler: h1.istio-system
    instances: ["i1"]
    # assign a name to the action
    name: a1
  requestHeaderOperations:
  # set "user" header to the output value of action "a1" in the request to httpbin
  - name: user
    values:
    - a1.output.user
  # remove "key" header from the request
  - name: key
    operation: REMOVE
  • Try sending a request without the key header:
curl -H Host:httpbin.example.com -v http://${INGRESS}/headers

You should see the same response as before, since the adapter issues an error code. Request and response operations are only applied to requests that pass checks successfully:

< HTTP/1.1 403 Forbidden
...
PERMISSION_DENIED:h1.handler.istio-system
  • Try sending a request with the header key set to foobar:
curl -H Host:httpbin.example.com -H key:foobar -v http://${INGRESS}/headers

The requests successfully reaches the back-end application this time:

> GET /headers HTTP/1.1
> Host:httpbin.example.com
>
< HTTP/1.1 200 OK
< server: istio-envoy
<
{
  "headers": {
    "Accept": "*/*",
    "Content-Length": "0",
    "Host": "httpbin.example.com",
    "User": "jason",
    "User-Agent": "curl/7.58.0",
    ...
  }
}

Note the absence of the header Key and addition of the header User with the value jason in the request received by the back-end application. The request operations are applied at the gateway before the request reaches httpbin.

Congratulations, you have successfully completed the first part of the guide, by implementing an adapter that modifies headers at the gateway!

Step 6: Try response header operations

We can also apply header operations on the response from the back-end application. Using kubectl, change the rule to the following snippet:

apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: r1
  namespace: istio-system
spec:
  # restrict the rule to the ingress gateway proxy workload only
  match: context.reporter.kind == "outbound" && source.labels["istio"] == "ingressgateway"
  actions:
  - handler: h1.istio-system
    instances: ["i1"]
    # assign a name to the action
    name: a1
  responseHeaderOperations:
  # set a response header
  - name: user
    values:
    - a1.output.user
  • Try sending a request without the key header:
curl -H Host:httpbin.example.com -v http://${INGRESS}/headers

Observe no changes to the behavior. Requests are denied, and no operations are applied:

PERMISSION_DENIED:h1.handler.istio-system
  • Try sending a request with the header key set to foobar:
curl -H Host:httpbin.example.com -H key:foobar -v http://${INGRESS}/headers

You should receive the following response:

> GET /headers HTTP/1.1
> Host:httpbin.example.com
> User-Agent: curl/7.58.0
> Accept: */*
> key:foobar
>
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Wed, 21 Nov 2018 00:38:33 GMT
< content-type: application/json
< content-length: 339
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 8
< user: jason
<
{
  "headers": {
    "Accept": "*/*",
    "Content-Length": "0",
    "Host": "httpbin.example.com",
    "Key": "foobar",
    "User-Agent": "curl/7.58.0",
    ...
  }
}

Note that the response header user is returned by the gateway, but no changes are made to the headers as received by the back-end application. The response header operations are applied at the gateway after the back-end application processed the request.

Step 7: What's next?

You can follow the rest of the adapter development guide to set up an integration test, or to package and distribute your custom adapter.

To clean up, delete the directory istio/userkey, and remove the configuration resources:

kubectl delete rule/r1 handler/h1 instance/i1 adapter/userkey template/userkey -n istio-system

Then, remove the adapter pod from the cluster:

kubectl delete deployment/userkey service/userkey -n istio-system

Dev Environment

Writing Code

Pull Requests

Testing

Performance

Releases

Misc

Central Istiod

Security

Mixer

Pilot

Telemetry

Clone this wiki locally