Skip to content

Mixer Out of Process Adapter Walkthrough

Pengyuan Bian edited this page Feb 28, 2019 · 6 revisions

This feature is available as alpha for Istio 1.0

This document walks through step-by-step instructions to implement, test and plug a simple gRPC adapter (adapter that runs outside Mixer process as a separate service).

This adapter:

  • Supports the metric template which ships with Mixer.

  • For every request, prints to a file the data it receives from Mixer at request time.

  • Runs as a separate service that Mixer interacts with.

It should take approximately 30 minutes to finish this task

You may look at Prometheus Out of process adapter for a working example

Before you start

You'll need Docker, Kubernetes, and Go installed to follow this walkthrough. If you need details see the Dev Guide.

Start Docker with Kubernetes enabled on local machine

Download a local copy of the Mixer repo:

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

Install protoc (version 3.5.1 or higher) from https://github.com/google/protobuf/releases and add it to your PATH

Set the MIXER_REPO variable to the path where the mixer repository is on the local machine. Also, $ISTIO should point to $GOPATH/src/istio.io.

export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer

export ISTIO=$GOPATH/src/istio.io

Successfully build the mixer server.

pushd $ISTIO/istio && make mixs

Step 1: Write basic adapter skeleton code

Create the mygrpcadapter directory and navigate to it.

cd $MIXER_REPO/adapter && mkdir mygrpcadapter && cd mygrpcadapter

Create the file named mygrpcadapter.go with the following content

It defines the adapter as a gRPC service that implements the service interface for metric template. This code so far does not add any functionality for printing details in a file. It is done in later steps.

// Copyright 2018 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package mygrpcadapter

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"

	"istio.io/api/mixer/adapter/model/v1beta1"
	"istio.io/istio/mixer/template/metric"
)

type (
	// Server is basic server interface
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	// MyGrpcAdapter supports metric template.
	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

var _ metric.HandleMetricServiceServer = &MyGrpcAdapter{}

// HandleMetric records metric entries
func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {
	return nil, nil
}

// Addr returns the listening address of the server
func (s *MyGrpcAdapter) Addr() string {
	return s.listener.Addr().String()
}

// Run starts the server run
func (s *MyGrpcAdapter) Run(shutdown chan error) {
	shutdown <- s.server.Serve(s.listener)
}

// Close gracefully shuts down the server; used for testing
func (s *MyGrpcAdapter) Close() error {
	if s.server != nil {
		s.server.GracefulStop()
	}

	if s.listener != nil {
		_ = s.listener.Close()
	}

	return nil
}

// NewMyGrpcAdapter creates a new IBP adapter that listens at provided port.
func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())
	s.server = grpc.NewServer()
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}

Just to ensure everything is good, let's build the code

go build ./...

Now we have the basic skeleton of an adapter with empty implementation for interfaces for the 'metric' templates. Later steps adds the core code for this adapter.

Step 2: Write adapter configuration

Since this adapter just prints the data it receives from Mixer into a file, the adapter configuration will take the path of that file as a configuration field.

Create the config proto file under the 'config' dir

mkdir config

Create a new config.proto file inside the config directory with the following content:

syntax = "proto3";

// config for mygrpcadapter
package adapter.mygrpcadapter.config;

import "gogoproto/gogo.proto";

option go_package="config";

// config for mygrpcadapter
message Params {
    // Path of the file to save the information about runtime requests.
    string file_path = 1;
}

Let's now generate the corresponding go file from the config.proto as well as the adapter resource that contains the adapter's info (configuration descriptor and name). To do this, add the following go generate comment to the adapter code. The bold text shows the new added text.



// nolint:lll
// Generates the mygrpcadapter adapter's resource yaml. It contains the adapter's configuration, name, 
// supported template names (metric in this case), and whether it is session or no-session based.
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter

..
..

Just to ensure everything is good, let's generate the file and build the code

go generate ./...
go build ./...

If nothing happens on go generate, make sure protoc is installed and is in path.

Note: The protoc command is executed inside a Docker container during the go generate command. If you will be running go generate within a container during your build process, ensure the file system matches the host and that the docker.sock file is mounted inside your container. For example: docker run -v ${HOME}:${HOME} -v /var/run/docker.sock:/var/run/docker.sock -e GOPATH=${HOME}/go -w ${HOME}/workspace/youradapter -it --rm yourcontainer:tag go generate ./...

Let's look at the generate files. config/mygrpcadapter.yaml is the adapter's resource that is provided to Mixer for it to know about this adapter. The resource name, mygrpcadapter in this case, is used by handler configurations to refer to the adapter.

# this config is created through command
# mixgen adapter -c $GOPATH/src/istio.io/istio/mixer/adapter/mygrpcadapter/config/config.proto_descriptor -o $GOPATH/src/istio.io/istio/mixer/adapter/mygrpcadapter/config -s=false -n mygrpcadapter -t metric
apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description:
  session_based: false
  templates:
  - metric
  Config: .......

Config.pb.go is the generated go file for the configuration proto Adapter. mysampleadapter.config.pb.html is the generated documentation for the adapter. Config.proto_descriptor is an intermediate file not directly used by the adapter code.

Step 3: Link adapter config with adapter code and add business logic

Modify the adapter code (mygrpcadapter.go) to use the adapter-specific configuration (defined in mygrpcadapter/config/config.proto) to instantiate the file to write to. The code also prints Instances in the file configured via handler configuration and includes auth related code which is covered by step 7. HandleMetric contains the new changes.

// Copyright 2018 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// nolint:lll
// Generates the mygrpcadapter adapter's resource yaml. It contains the adapter's configuration, name, supported template
// names (metric in this case), and whether it is session or no-session based.
//go:generate $GOPATH/src/istio.io/istio/bin/mixer_codegen.sh -a mixer/adapter/mygrpcadapter/config/config.proto -x "-s=false -n mygrpcadapter -t metric"

package mygrpcadapter

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"bytes"
	"os"

	"istio.io/api/mixer/adapter/model/v1beta1"
	policy "istio.io/api/policy/v1beta1"
	"istio.io/istio/mixer/adapter/mygrpcadapter/config"
	"istio.io/istio/mixer/template/metric"
	"istio.io/istio/pkg/log"
)

type (
	// Server is basic server interface
	Server interface {
		Addr() string
		Close() error
		Run(shutdown chan error)
	}

	// MyGrpcAdapter supports metric template.
	MyGrpcAdapter struct {
		listener net.Listener
		server   *grpc.Server
	}
)

var _ metric.HandleMetricServiceServer = &MyGrpcAdapter{}

// HandleMetric records metric entries
func (s *MyGrpcAdapter) HandleMetric(ctx context.Context, r *metric.HandleMetricRequest) (*v1beta1.ReportResult, error) {

	log.Infof("received request %v\n", *r)
	var b bytes.Buffer
	cfg := &config.Params{}

	if r.AdapterConfig != nil {
		if err := cfg.Unmarshal(r.AdapterConfig.Value); err != nil {
			log.Errorf("error unmarshalling adapter config: %v", err)
			return nil, err
		}
	}

	b.WriteString(fmt.Sprintf("HandleMetric invoked with:\n  Adapter config: %s\n  Instances: %s\n",
		cfg.String(), instances(r.Instances)))

	if cfg.FilePath == "" {
		fmt.Println(b.String())
	} else {
		_, err := os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
		if err != nil {
			log.Errorf("error creating file: %v", err)
		}
		f, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_WRONLY, 0600)
		if err != nil {
			log.Errorf("error opening file for append: %v", err)
		}

		defer f.Close()

		log.Infof("writing instances to file %s", f.Name())
		if _, err = f.Write(b.Bytes()); err != nil {
			log.Errorf("error writing to file: %v", err)
		}
	}

	log.Infof("success!!")
	return &v1beta1.ReportResult{}, nil
}

func decodeDimensions(in map[string]*policy.Value) map[string]interface{} {
	out := make(map[string]interface{}, len(in))
	for k, v := range in {
		out[k] = decodeValue(v.GetValue())
	}
	return out
}

func decodeValue(in interface{}) interface{} {
	switch t := in.(type) {
	case *policy.Value_StringValue:
		return t.StringValue
	case *policy.Value_Int64Value:
		return t.Int64Value
	case *policy.Value_DoubleValue:
		return t.DoubleValue
	default:
		return fmt.Sprintf("%v", in)
	}
}

func instances(in []*metric.InstanceMsg) string {
	var b bytes.Buffer
	for _, inst := range in {
		b.WriteString(fmt.Sprintf("'%s':\n"+
			"  {\n"+
			"		Value = %v\n"+
			"		Dimensions = %v\n"+
			"  }", inst.Name, decodeValue(inst.Value.GetValue()), decodeDimensions(inst.Dimensions)))
	}
	return b.String()
}

// Addr returns the listening address of the server
func (s *MyGrpcAdapter) Addr() string {
	return s.listener.Addr().String()
}

// Run starts the server run
func (s *MyGrpcAdapter) Run(shutdown chan error) {
	shutdown <- s.server.Serve(s.listener)
}

// Close gracefully shuts down the server; used for testing
func (s *MyGrpcAdapter) Close() error {
	if s.server != nil {
		s.server.GracefulStop()
	}

	if s.listener != nil {
		_ = s.listener.Close()
	}

	return nil
}

func getServerTLSOption(credential, privateKey, caCertificate string) (grpc.ServerOption, error) {
	certificate, err := tls.LoadX509KeyPair(
		credential,
		privateKey,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to load key cert pair")
	}
	certPool := x509.NewCertPool()
	bs, err := ioutil.ReadFile(caCertificate)
	if err != nil {
		return nil, fmt.Errorf("failed to read client ca cert: %s", err)
	}

	ok := certPool.AppendCertsFromPEM(bs)
	if !ok {
		return nil, fmt.Errorf("failed to append client certs")
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{certificate},
		ClientCAs:    certPool,
	}
	tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert

	return grpc.Creds(credentials.NewTLS(tlsConfig)), nil
}

// NewMyGrpcAdapter creates a new IBP adapter that listens at provided port.
func NewMyGrpcAdapter(addr string) (Server, error) {
	if addr == "" {
		addr = "0"
	}
	listener, err := net.Listen("tcp", fmt.Sprintf(":%s", addr))
	if err != nil {
		return nil, fmt.Errorf("unable to listen on socket: %v", err)
	}
	s := &MyGrpcAdapter{
		listener: listener,
	}
	fmt.Printf("listening on \"%v\"\n", s.Addr())

	credential := os.Getenv("GRPC_ADAPTER_CREDENTIAL")
	privateKey := os.Getenv("GRPC_ADAPTER_PRIVATE_KEY")
	certificate := os.Getenv("GRPC_ADAPTER_CERTIFICATE")
	if credential != "" {
		so, err := getServerTLSOption(credential, privateKey, certificate)
		if err != nil {
			return nil, err
		}
		s.server = grpc.NewServer(so)
	} else {
		s.server = grpc.NewServer()
	}
	metric.RegisterHandleMetricServiceServer(s.server, s)
	return s, nil
}

Just to ensure everything is good, let's build the code

go build ./...

Let's also write the main function that can start the gRPC service. Create a file cmd/main.go with the following content

// Copyright 2018 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"fmt"
	"os"

	mygrpcadapter "istio.io/istio/mixer/adapter/mygrpcadapter"
)

func main() {
	addr := ""
	if len(os.Args) > 1 {
		addr = os.Args[1]
	}

	s, err := mygrpcadapter.NewMyGrpcAdapter(addr)
	if err != nil {
		fmt.Printf("unable to start server: %v", err)
		os.Exit(-1)
	}

	shutdown := make(chan error, 1)
	go func() {
		s.Run(shutdown)
	}()
	_ = <-shutdown
}

This concludes the implementation part of the adapter code. Next steps show how to plug an adapter into a build of Mixer and to verify your code's behavior.

Step 4: Write sample operator config

To see if your adapter works, we will need a sample operator configuration. So, let's write a simple operator configuration that we will give to Mixer for it to dispatch data to your sample adapter. We will need instance, handler and rule configuration to be passed to the Mixers configuration server.

Create a sample operator config file with name sample_operator_cfg.yaml inside the $MIXER_REPO/adapter/mygrpcadapter directory with the following content:

Add the following content to the file $MIXER_REPO/adapter/mygrpcadapter/sample_operator_cfg.yaml

# handler for adapter mygrpcadapter
apiVersion: "config.istio.io/v1alpha2"
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 adapter: mygrpcadapter
 connection:
   address: "{ADDRESS}" #replaces at runtime by the test
 params:
   file_path: "out.txt"
---

# instance for template metric
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
 name: i1metric
 namespace: istio-system
spec:
 template: metric
 params:
   value: request.size | 0
   dimensions:
     response_code: "200"
---

# rule to dispatch to handler h1
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
 name: r1
 namespace: istio-system
spec:
 actions:
 - handler: h1.istio-system
   instances:
   - i1metric
---

Step 5: Start Mixer and validate the adapter.

First let's copy the configs associated with mygrpcadapter into a separate directory from where Mixer can read it along with other important sample resources like attribute vocabulary.

mkdir testdata
cp sample_operator_cfg.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
cp config/mygrpcadapter.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/testdata/config/attributes.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/template/metric/template.yaml $MIXER_REPO/adapter/mygrpcadapter/testdata

Now let's start the gRPC adapter. On a new terminal run

export ISTIO=$GOPATH/src/istio.io
export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
cd $MIXER_REPO/adapter/mygrpcadapter
go run cmd/main.go

Note the listening address printed on the console. Copy the content within the quota. For below output the listener address is [::]:38355 listening on "[::]:38355"

On a new terminal export the listener address in an ADDRESS variable.

export ISTIO=$GOPATH/src/istio.io
export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
export ADDRESS=[::]:38355

Change the handler's connection parameters to point to the adapter address $ADDRESS. Replace the {ADDRESS} string in testdata/sample_operator_cfg.yaml with value of $ADDRESS

sed -i -e "s/{ADDRESS}/${ADDRESS}/g" $MIXER_REPO/adapter/mygrpcadapter/testdata/sample_operator_cfg.yaml

Start the mixer pointing it to the testdata operator configuration

pushd $ISTIO/istio && make mixs
// locate mixs binary, should be $GOPATH/out/linux_amd64/release/mixs on linux os and 
// $GOPATH/out/darwin_amd64/release/mixs on mac os. 
// Choose command below according to your os:
$GOPATH/out/linux_amd64/release/mixs server --configStoreURL=fs://$(pwd)/mixer/adapter/mygrpcadapter/testdata

The terminal will have the following output and will be blocked waiting to serve requests. There might be errors related to other unrelated configurations; we can ignore them for now.

..
..
Mixer started with
MaxMessageSize: 1048576
MaxConcurrentStreams: 1024
APIWorkerPoolSize: 1024
AdapterWorkerPoolSize: 1024
ExpressionEvalCacheSize: 1024
APIPort: 9091
MonitoringPort: 9093
SingleThreaded: false
ConfigStore2URL: fs:///usr/local/google/home/guptasu/go/src/istio.io/istio/mixer/adapter/mygrpcadapter/sampleoperatorconfig
ConfigDefaultNamespace: istio-system
ConfigIdentityAttribute: destination.service
ConfigIdentityAttributeDomain: svc.cluster.local
LoggingOptions: log.Options{OutputPaths:[]string{"stdout"}, ErrorOutputPaths:[]string{"stderr"}, RotateOutputPath:"", RotationMaxSize:104857600, RotationMaxAge:30, RotationMaxBackups:1000, JSONEncoding:false, IncludeCallerSourceLocation:false, stackTraceLevel:"none", outputLevel:"info"}
TracingOptions: tracing.Options{ZipkinURL:"", JaegerURL:"", LogTraceSpans:false}

2018-01-06T01:43:12.305995Z	info	template Kind: kubernetesenv, &InstanceParam{SourceUid:,SourceIp:,DestinationUid:,DestinationIp:,OriginUid:,OriginIp:,AttributeBindings:map[string]string{},}
...

Now let's call 'report' using mixer client. This step should cause the mixer server to call your sample adapter with instance objects constructed using the operator configuration.

On a new terminal, set the $ISTIO and $MIXER_REPO variables.

export ISTIO=$GOPATH/src/istio.io
export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer

Then run the following.

pushd $ISTIO/istio && make mixc
// locate mixc binary, should be $GOPATH/out/linux_amd64/release/mixc on linux os and 
// $GOPATH/out/darwin_amd64/release/mixc on mac os. 
// Choose command below according to your os:
$GOPATH/out/linux_amd64/release/mixc report -s destination.service="svc.cluster.local" -i request.size=1235

Inspect the out.txt file that your adapter would have printed. If you have followed the above steps, then the out.txt should be in your $MIXER_REPO/adapter/mygrpcadapter/ directory.

tail $MIXER_REPO/adapter/mygrpcadapter/out.txt

You should see something like:

HandleMetric invoked with:
  Adapter config: &Params{FilePath:out.txt,}
  Instances: 'i1metric.instance.istio-system':
  {
		Value = 1235
		Dimensions = map[response_code:200]
  }

You can even try passing other attributes to mixer server and inspect your out.txt file to see how the data passed to the adapter changes. For example,

pushd $ISTIO/istio && make mixc && mixc report -s destination.service="svc.cluster.local" -i request.size=9999999999

If you have reached this far, congratulate yourself !!. You have successfully created a Mixer adapter. You can close (ctrl + c) on your terminals that were running mixer server and gRPC adapter to shut them down.

Step 6: Write test and validate your adapter.

The above steps 5 (start mixer server and validate ..) were mainly to test your adapter code. You can achieve the same thing by writing a simple test that uses the Mixer's 'pkg/adapter/test' package to start an in-proc Mixer and make calls to it via mixer client.

Add a test file for your adapter code

touch $MIXER_REPO/adapter/mygrpcadapter/mygrpcadapter_integration_test.go

Add the following content to that file

package mygrpcadapter

import (
  "fmt"
  "io/ioutil"
  "testing"

  adapter_integration "istio.io/istio/mixer/pkg/adapter/test"
  "os"
  "strings"
)

func TestReport(t *testing.T) {
  adptCrBytes, err := ioutil.ReadFile("config/mygrpcadapter.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }

  operatorCfgBytes, err := ioutil.ReadFile("sample_operator_cfg.yaml")
  if err != nil {
     t.Fatalf("could not read file: %v", err)
  }
  operatorCfg := string(operatorCfgBytes)
  shutdown := make(chan error, 1)

  var outFile *os.File
  outFile, err = os.OpenFile("out.txt", os.O_RDONLY|os.O_CREATE, 0666)
  if err != nil {
     t.Fatal(err)
  }
  defer func() {
     if removeErr := os.Remove(outFile.Name()); removeErr != nil {
        t.Logf("Could not remove temporary file %s: %v", outFile.Name(), removeErr)
     }
  }()

  adapter_integration.RunTest(
     t,
     nil,
     adapter_integration.Scenario{
        Setup: func() (ctx interface{}, err error) {
           pServer, err := NewMyGrpcAdapter("")
           if err != nil {
              return nil, err
           }
           go func() {
              pServer.Run(shutdown)
              _ = <-shutdown
           }()
           return pServer, nil
        },
        Teardown: func(ctx interface{}) {
           s := ctx.(Server)
           s.Close()
        },
        ParallelCalls: []adapter_integration.Call{
           {
              CallKind: adapter_integration.REPORT,
              Attrs:    map[string]interface{}{"request.size": int64(555)},
           },
        },
        GetState: func(ctx interface{}) (interface{}, error) {
           // validate if the content of "out.txt" is as expected
           bytes, err := ioutil.ReadFile("out.txt")
           if err != nil {
              return nil, err
           }
           s := string(bytes)
           wantStr := `HandleMetric invoked with:
       Adapter config: &Params{FilePath:out.txt,}
       Instances: 'i1metric.instance.istio-system':
       {
           Value = 555
           Dimensions = map[response_code:200]
       }
`
           if normalize(s) != normalize(wantStr) {
              return nil, fmt.Errorf("got adapters state as : '%s'; want '%s'", s, wantStr)
           }
           return nil, nil
        },
        GetConfig: func(ctx interface{}) ([]string, error) {
           s := ctx.(Server)
           return []string{
              // CRs for built-in templates (metric is what we need for this test)
              // are automatically added by the integration test framework.
              string(adptCrBytes),
              strings.Replace(operatorCfg, "{ADDRESS}", s.Addr(), 1),
           }, nil
        },
        Want: `
     {
      "AdapterState": null,
      "Returns": [
       {
        "Check": {
         "Status": {},
         "ValidDuration": 0,
         "ValidUseCount": 0
        },
        "Quota": null,
        "Error": null
       }
      ]
     }`,
     },
  )
}

func normalize(s string) string {
  s = strings.TrimSpace(s)
  s = strings.Replace(s, "\t", "", -1)
  s = strings.Replace(s, "\n", "", -1)
  s = strings.Replace(s, " ", "", -1)
  return s
}

Now run the test

cd $MIXER_REPO/adapter/mygrpcadapter && go build ./... && go test *.go

The test starts a inproc mixer, provides it with all the configuration to route metrics to the mygrpcadapter and invoke it via mixer client.

Step 7: Encrypt connection between Mixer and GRPC adapter

This is an optional step and this feature is introduced in release 1.1. Istio provides Mutual TLS feature to secure connection between workloads, which is also supported by Mixer for connections to GRPC adpater. mygrpcadapter code already supports MTLS. It loads key/cert from files passed in by environment variables and requires Mixer to present certificate if credential is provided. To test it out, we need some sample key cert. Mixer code base already has some test key certs and we will reuse them in this step.

export MIXER_REPO=$GOPATH/src/istio.io/istio/mixer
mkdir /tmp/grpc-test-key-cert
cp -R $MIXER_REPO/pkg/protobuf/yaml/testdata/auth/. /tmp/grpc-test-key-cert

Then export environment variables that are used by GRPC adapter

export GRPC_ADAPTER_CREDENTIAL=/tmp/grpc-test-key-cert/adapter.crt
export GRPC_ADAPTER_PRIVATE_KEY=/tmp/grpc-test-key-cert/adapter.key
export GRPC_ADAPTER_CERTIFICATE=/tmp/grpc-test-key-cert/ca.pem

After exporting those environment variables, restart the adapter in the same terminal and adjust address in handler config accordingly

go run cmd/main.go

Mixer also needs to load key certs, which could be configured via adapter handler config, replace your test operator config $MIXER_REPO/adapter/mygrpcadapter/testdata/sample_operator_cfg.yaml with the following

# handler for adapter mygrpcadapter
apiVersion: "config.istio.io/v1alpha2"
kind: handler
metadata:
 name: h1
 namespace: istio-system
spec:
 adapter: mygrpcadapter
 connection:
  address: "{ADDRESS}" #replaces at runtime by the test
  authentication:
    mutual:
      private_key: "/tmp/grpc-test-key-cert/mixer.key"
      client_certificate: "/tmp/grpc-test-key-cert/mixer.crt"
      ca_certificates: "/tmp/grpc-test-key-cert/ca.pem"
 params:
   file_path: "out.txt"
---
# instance for template metric
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
 name: i1metric
 namespace: istio-system
spec:
 template: metric
 params:
   value: request.size | 0
   dimensions:
     response_code: "200"
---
# rule to dispatch to handler h1
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
 name: r1
 namespace: istio-system
spec:
 actions:
 - handler: h1.istio-system
   instances:
   - i1metric
---

Then restart Mixer with the new config, and now communication between Mixer and adapter should be MTLS encrypted. Send sample report traffic with mixc as former step to verify that the connection works.

Note: within Istio installation, Mixer mounts a key/cert pair by default, which is generated by citadel and uses SAN/SPIFFE base on Mixer service account as identity. In release-1.1, Mixer enforces that certificate from the adapter must be of the same identity as Mixer, which could be achieved by running adapter with Mixer service account. You can load key/cert in adapter either via manual volume mount or sidecar injection. Besides MTLS, TLS is also supported if your GRPC adpater runs outside of mesh or if you are using adapter services provided by vendor. See here for more detail on TLS related config.

Step 8: Cleanup

Delete the adapter/mygrpcadapter directory

Clone this wiki locally
You can’t perform that action at this time.