10.2 实现适配器

在开始之前,确定已经安装了Docker、Kubernetes和Go语言环境,并设置GOPATH环境变量。下载Mixer代码仓库的本地副本,命令如下:


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

https://github.com/google/protobuf/releases安装protoc(版本3.5.1或更高版本)并将其添加到环境变量PATH中。

将MIXER_REPO变量设置为Mixer代码存储库在本地计算机上的路径。另外,设置环境变量$ISTIO,使之指向$GOPATH/src/istio.io。设置如下环境变量:


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

运行如下命令,如果成功运行则表明环境准备通过:


pushd $ISTIO/istio && make mixs

接下来,开发一个简单的定制化的Out of Process(进程外)授权适配器,我们将其用于处理授权检查。Istio中已经自带了基本的身份验证和授权能力,但用户可以将自定义授权作为一种定制化的策略适配器直接注入Mixer中。实现的基本思路是设置一个独立于Mixer的外部服务,该服务接受来自入站请求的头信息,然后做出是否允许请求通过或不通过的决定。

此外,可以将定制的外部适配器作为单独的Kubernetes服务运行,也可以将其完全运行在集群外部。对于任何一种情况,授权服务器都应验证入站请求并保护其端点(此示例仅使用明文未加密的gRPC作为演示)。

开发一个定制化的适配器包括以下几个步骤:

·步骤1:编写基本适配器框架代码

·步骤2:编写适配器配置

·步骤3:链接适配器代码与配置并添加业务逻辑

·步骤4:编写示例运维配置

·步骤5:启动Mixer并验证适配器

步骤1:编写基本适配器框架代码

创建mygrpcadapter目录并导航到它,如下所示:


cd $MIXER_REPO/adapter 
mkdir mygrpcadapter  
cd mygrpcadapter

创建名为mygrpcadapter.go的文件。在该文件中,将适配器定义为实现授权模板的服务接口的gRPC服务。到目前为止,代码并没有添加任何关于验证授权逻辑的详细功能,它是在后面的步骤中完成的。文件的内容如下所示:


package mygrpcadapter

import (
  "context"
  "fmt"
  "net"

  "google.golang.org/grpc"

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

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 _ authorization.HandleAuthorizationServiceServer = &MyGrpcAdapter{}

func (s *MyGrpcAdapter) HandleAuthorization(ctx context.Context, r *authorization.
HandleAuthorizationRequest) (*v1beta1.CheckResult, 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()
  authorization.RegisterHandleAuthorizationServiceServer(s.server, s)
  return s, nil
}

为了确保上述代码准确无误,通过以下命令构建代码:


go build ./...

现在我们有了一个适配器的基本框架,其中包含授权模板接口的空的实现。后面的步骤将添加此适配器的核心代码。

步骤2:编写适配器配置

由于此适配器只是对从Mixer接收的数据进行校验,因此适配器配置将该文件的路径作为配置字段。

在conf ig目录下创建配置proto文件,如下所示:


mkdir config
touch config.proto

使用以下内容在conf ig目录中创建新的conf ig.proto文件:


syntax = "proto3";

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

import "gogoproto/gogo.proto";

option go_package="config";

// config for mygrpcadapter
message Params {
  
    // header name to check for custom token
    string auth_key = 1;
}

文件conf ig.proto可以用来生成相应的go文件以及包含适配器信息(配置描述符和名称)的适配器资源。为此,请在适配器代码中添加以下go generate注释:


// 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 authorization"
package mygrpcadapter

import (
  "context"
  "fmt"
  "net"

  "google.golang.org/grpc"

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

....
....

go generate过程需要拉取镜像gcr.io/istio-testing/protoc:2018-06-12,由于各种原因不能正常拉取的话,可以将文件$ISTIO/istio/bin/protoc.sh中的代码gen_img=gcr.io/istio-testing/protoc:2018-06-12替换为:


gen_img=osswangxining/gcr.io-istio-testing-protoc:latest

为了确保上述代码准确无误,通过以下命令构建代码:


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

第一次运行应该会出现如下类似的结果:


Unable to find image 'osswangxining/gcr.io-istio-testing-protoc:latest' locally
latest: Pulling from osswangxining/gcr.io-istio-testing-protoc
ff3a5c916c92: Already exists
dc6fb857355d: Pulling fs layer
f198fee4ce54: Pulling fs layer
faa0691ae748: Pulling fs layer
faa0691ae748: Verifying Checksum
faa0691ae748: Download complete
dc6fb857355d: Verifying Checksum
dc6fb857355d: Download complete
dc6fb857355d: Pull complete
f198fee4ce54: Verifying Checksum
f198fee4ce54: Download complete
f198fee4ce54: Pull complete
faa0691ae748: Pull complete
Digest: sha256:1c492efce28b962c49bb3f2eedcece3c2ce0fd7f7e2752a7568d71f5cdeceec5
Status: Downloaded newer image for osswangxining/gcr.io-istio-testing-protoc:latest

在conf ig目录下会生成如下文件:


config
├── adapter.mygrpcadapter.config.pb.html
├── config.pb.go
├── config.proto
├── config.proto_descriptor
└── mygrpcadapter.yaml

如果在生成过程中没有任何反应,请确保已安装protoc程序,并且将其设置到执行路径中。

conf ig/mygrpcadapter.yaml是适配器的资源文件,提供给Mixer以便让它了解此适配器。在这种情况下,处理程序配置中使用资源名称mygrpcadapter来引用该适配器。文件内容如下所示:


# 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 authorization
apiVersion: "config.istio.io/v1alpha2"
kind: adapter
metadata:
  name: mygrpcadapter
  namespace: istio-system
spec:
  description: 
  session_based: false
  templates:
  - authorization
  config: .......

Conf ig.pb.go是配置适配器的生成的go文件。mysampleadapter.conf ig.pb.html是生成的适配器文档。Conf ig.proto_descriptor是适配器代码中不直接使用的中间文件。

步骤3:链接适配器代码与配置并添加业务逻辑

修改适配器代码(mygrpcadapter.go)以使用特定于适配器的配置(具体就是指在文件mygrpcadapter/conf ig/conf ig.proto中定义的配置),HandleAuthorization函数包含了具体的新代码的更改,如下所示:


// 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 authorization"

package mygrpcadapter

import (
  "context"
  "fmt"
  "net"

  "google.golang.org/grpc"

  "istio.io/api/mixer/adapter/model/v1beta1"
  policy "istio.io/api/policy/v1beta1"
  "istio.io/istio/mixer/adapter/mygrpcadapter/config"
  "istio.io/istio/mixer/pkg/status"
  "istio.io/istio/mixer/template/authorization"
  "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 _ authorization.HandleAuthorizationServiceServer = &MyGrpcAdapter{}

// HandleMetric records metric entries
func (s *MyGrpcAdapter) HandleAuthorization(ctx context.Context, r *authorization.HandleAuthorizationRequest) (*v1beta1.CheckResult, error) {

  **log.Infof("received request %v\n", *r)

  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
    }
  }

  decodeValue := func(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)
    }
  }

  decodeValueMap := func(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
  }

  log.Infof(cfg.AuthKey)

  props := decodeValueMap(r.Instance.Subject.Properties)
  log.Infof("%v", props)

  for k, v := range props {
    fmt.Println("k:", k, "v:", v)
    if (k == "custom_token_header") && v == cfg.AuthKey {
      log.Infof("success!!")
      return &v1beta1.CheckResult{
        Status: status.OK,
      }, nil
    }
  }

  log.Infof("failure; header not provided")
  return &v1beta1.CheckResult{
    Status: status.WithPermissionDenied("Unauthorized..."),
  }, 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()
  authorization.RegisterHandleAuthorizationServiceServer(s.server, s)
  return s, nil
}

为了确保上述代码准确无误,通过以下命令构建代码:


go build ./...

接下来还要编写main程序以用于启动gRPC服务。创建cmd/main.go文件,包含以下内容:


// 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
}

这样就完成了适配器代码的实现部分。接下来的步骤演示如何将适配器插入到Mixer的构建中并验证代码逻辑。

步骤4:编写示例配置

为了验证适配器是否正确运行,我们需要一个示例配置。这个简单的配置文件将传递给Mixer,并将数据分发到该示例适配器。我们需要将实例、处理程序和规则配置传递给Mixer配置服务器。

在$MIXER_REPO/adapter/mygrpcadapter目录中创建一个命名为sample_operator_cfg.yaml的示例配置文件,其中包含以下内容:


# handler for adapter mygrpcadapter
apiVersion: "config.istio.io/v1alpha2"
kind: handler
metadata:
 name: myhandler4auth
 namespace: istio-system
spec:
 adapter: mygrpcadapter
 connection:
   address: "{ADDRESS}"
   #address: "mygrpcadapterservice:46990"
 params:
   auth_key: "abc"
---
apiVersion: "config.istio.io/v1alpha2"
kind: instance
metadata:
 name: mycheck
 namespace: istio-system
spec:
 template: authorization
 params:
   subject:
     properties:
       custom_token_header:  request.headers["x-custom-token"]
---

# rule to dispatch to handler myhandler
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
 name: myrule
 namespace: istio-system
spec:
 actions:
 - handler: myhandler4auth.istio-system
   instances:
   - mycheck
---

步骤5:启动Mixer并验证适配器

首先将与mygrpcadapter相关的配置复制到一个单独的目录中,Mixer可以一起读取这些重要的示例资源,包括属性词汇表等,命令如下所示:


mkdir -p $MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/adapter/mygrpcadapter/config/mygrpcadapter.yaml
$MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/testdata/config/attributes.yaml
$MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/template/authorization/template.yaml
$MIXER_REPO/adapter/mygrpcadapter/testdata
cp $MIXER_REPO/adapter/mygrpcadapter/sample_operator_cfg.yaml
$MIXER_REPO/adapter/mygrpcadapter/testdata

在testdata目录下会生成如下文件:


tree testdata
testdata
├── attributes.yaml
├── mygrpcadapter.yaml
├── sample_operator_cfg.yaml
└── template.yaml

0 directories, 4 files

现在让我们启动gRPC适配器。在新的终端窗口中运行如下命令:


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

请注意控制台上打印的监听地址。对于下面的输出,侦听器地址是[::]:53814:


listening on "[::]:53814"

打开一个新终端窗口,并在其中设置ADDRESS变量指向上述的监听器地址。


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

更改处理程序的连接参数以指向适配器地址$ADDRESS。将testdata/sample_operator_cfg.yaml中的{ADDRESS}字符串替换为$ADDRESS的值,如下所示:


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

启动Mixer,将其指向testdata配置,如下所示:


pushd $ISTIO/istio && make mixs
//找到mixs二进制文件,如果是linux操作系统,路径为$GOPATH/out/linux_amd64/release/mixs,
// 如果是mac os操作系统,路径则为$GOPATH/out/darwin_amd64/release/mixs 
// 根据你的操作系统选择以下命令:
$GOPATH/out/darwin_amd64/release/mixs server
--configStoreURL=fs://$(pwd)/mixer/adapter/mygrpcadapter/testdata
--log_output_level=attributes:debug

终端将具有以下输出,并将被阻塞以等待服务请求,可能会存在与其他无关配置相关的错误,我们现在可以忽略它们:


Mixer started with
MaxMessageSize:  1048576
MaxConcurrentStreams:  1024
APIWorkerPoolSize:  1024
AdapterWorkerPoolSize:  1024
APIPort:  9091
APIAddress:
MonitoringPort:  9093
EnableProfiling:  true
SingleThreaded:  false
NumCheckCacheEntries:  1500000
ConfigStoreURL:  fs:///Users/wangxn/Documents/mygoworkspace/src/istio.io/istio/mixer/adapter/mygrpcadapter/testdata
CertificateFile:  /etc/certs/cert-chain.pem
KeyFile:  /etc/certs/key.pem
CACertificateFile:  /etc/certs/root-cert.pem
ConfigDefaultNamespace:  istio-system
LoggingOptions: log.Options{OutputPaths:[]string{"stdout"}, ErrorOutputPaths:[]string{"stderr"}, RotateOutputPath:"", RotationMaxSize:104857600, RotationMaxAge:30, RotationMaxBackups:1000, JSONEncoding:false, LogGrpc:true, outputLevels:"attributes:debug", logCallers:"", stackTraceLevels:"default:none"}
TracingOptions: tracing.Options{ZipkinURL:"", JaegerURL:"", LogTraceSpans:false, SamplingRate:0}
IntrospectionOptions: ctrlz.Options{Port:0x2694, Address:"127.0.0.1"}
LoadSheddingOptions: loadshedding.Options{Mode:0, AverageLatencyThreshold:0, SamplesPerSecond:1.7976931348623157e+308, SampleHalfLife:1000000000, MaxRequestsPerSecond:0, BurstSize:0}

2019-02-17T05:06:49.026405Z info  Awaiting for config store sync...
2019-02-17T05:06:49.026439Z info  Starting runtime config watch...
2019-02-17T05:06:49.026515Z info  Built new config.Snapshot: id='0'
2019-02-17T05:06:49.026568Z info  Cleaning up handler table, with config ID:-1
2019-02-17T05:06:49.041592Z info  Built new config.Snapshot: id='1'
2019-02-17T05:06:49.043032Z info  parsed scheme: ""
2019-02-17T05:06:49.043051Z info  scheme "" not registered, fallback to default scheme
2019-02-17T05:06:49.043070Z info  grpcAdapter Connected to: [::]:53814
2019-02-17T05:06:49.043107Z info  Cleaning up handler table, with config ID:0
2019-02-17T05:06:49.043182Z info  Starting monitor server...
2019-02-17T05:06:49.043181Z info  ccResolverWrapper: sending new addresses to cc: [{[::]:53814 0  <nil>}]
2019-02-17T05:06:49.043204Z info  ClientConn switching balancer to "pick_first"
2019-02-17T05:06:49.043276Z info  pickfirstBalancer: HandleSubConnStateChange: 0xc000bf00b0, CONNECTING
2019-02-17T05:06:49.043745Z info  pickfirstBalancer: HandleSubConnStateChange: 0xc000bf00b0, READY
Istio Mixer: wangxn@ali-1c36bbed0b91.local-docker.io/istio-d3b598579ec4d395e6c26ac6b468a3ea5315efbc-dirty-d3b598579ec4d395e6c26ac6b468a3ea5315efbc-dirty-Modified
Starting gRPC server on port 9091
2019-02-17T05:06:49.045417Z info  ControlZ available at 192.168.1.9:9876
...

现在让我们使用Mixer客户端调用该适配器。此步骤中调用示例适配器的应该是Mixer服务器使用规则配置构造的实例对象。

打开一个新终端窗口,并设置$ISTIO和$MIXER_REPO变量,如下所示:


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

然后运行以下命令:


pushd $ISTIO/istio && make mixc
//找到mixc二进制文件,如果是linux操作系统,路径为$GOPATH/out/linux_amd64/release/mixs,
// 如果是mac os操作系统,路径则为$GOPATH/out/darwin_amd64/release/mixs 
// 根据你的操作系统选择以下命令:
$GOPATH/out/darwin_amd64/release/mixc check -s destination.service="svc.cluster.local" --stringmap_attributes "request.headers=x-custom-token:abc"

在mixs运行的终端窗口中,可以看到类似于如下的内容:


2019-02-17T05:07:11.237915Z debug attributes  Creating bag with attributes: destination.service           : svc.cluster.local
request.headers               : stringmap[x-custom-token:abc]

而在mixc运行的终端窗口中,可以看到类似于如下的内容:


Check RPC completed successfully. Check status was OK
  Valid use count: 10000, valid duration: 1m0s
  Referenced Attributes
    context.reporter.kind ABSENCE
    destination.namespace ABSENCE
    request.headers::x-custom-token EXACT

现在不发送请求头或不正确的请求头值,如下所示:


pushd $ISTIO/istio && make mixc
// 找到mixc二进制文件,如果是linux操作系统,路径为$GOPATH/out/linux_amd64/release/mixs,
// 如果是mac os操作系统,路径则为$GOPATH/out/darwin_amd64/release/mixs 
// 根据你的操作系统选择以下命令:
$GOPATH/out/darwin_amd64/release/mixc check -s destination.service="svc.cluster.local" --stringmap_attributes "request.headers=x-custom-token:wrongvalue"

在mixc运行的终端窗口中,可以看到检查状态为PERMISSION_DENIED的如下内容:


Check RPC completed successfully. Check status was PERMISSION_DENIED (myhandler4auth.handler.istio-system:Unauthorized...)
  Valid use count: 0, valid duration: 0s
  Referenced Attributes
    context.reporter.kind ABSENCE
    destination.namespace ABSENCE
    request.headers::x-custom-token EXACT

到此为止,如果上述步骤全部执行成功并得到预期的结果,那么说明我们已成功创建Mixer适配器。接下来将其部署到Kubernetes集群中以便在Istio中使用这个适配器的功能。