learn-tech/专栏/Go语言项目开发实战/34SDK设计(下):IAM项目GoSDK设计和实现.md
2024-10-16 00:01:16 +08:00

644 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
34 SDK 设计IAM项目Go SDK设计和实现
你好,我是孔令飞。
上一讲我介绍了公有云厂商普遍采用的SDK设计方式。其实还有一些比较优秀的SDK设计方式比如 Kubernetes的 client-go SDK设计方式。IAM项目参考client-go也实现了client-go风格的SDKmarmotedu-sdk-go。
和 33讲 介绍的SDK设计方式相比client-go风格的SDK具有以下优点
大量使用了Go interface特性将接口的定义和实现解耦可以支持多种实现方式。
接口调用层级跟资源的层级相匹配,调用方式更加友好。
多版本共存。
所以我更推荐你使用marmotedu-sdk-go。接下来我们就来看下marmotedu-sdk-go是如何设计和实现的。
marmotedu-sdk-go设计
和medu-sdk-go相比marmotedu-sdk-go的设计和实现要复杂一些但功能更强大使用体验也更好。
这里我们先来看一个使用SDK调用iam-authz-server /v1/authz 接口的示例,代码保存在 marmotedu-sdk-go/examples/authz_clientset/main.go文件中
package main
import (
"context"
"flag"
"fmt"
"path/filepath"
"github.com/ory/ladon"
metav1 "github.com/marmotedu/component-base/pkg/meta/v1"
"github.com/marmotedu/component-base/pkg/util/homedir"
"github.com/marmotedu/marmotedu-sdk-go/marmotedu"
"github.com/marmotedu/marmotedu-sdk-go/tools/clientcmd"
)
func main() {
var iamconfig *string
if home := homedir.HomeDir(); home != "" {
iamconfig = flag.String(
"iamconfig",
filepath.Join(home, ".iam", "config"),
"(optional) absolute path to the iamconfig file",
)
} else {
iamconfig = flag.String("iamconfig", "", "absolute path to the iamconfig file")
}
flag.Parse()
// use the current context in iamconfig
config, err := clientcmd.BuildConfigFromFlags("", *iamconfig)
if err != nil {
panic(err.Error())
}
// create the clientset
clientset, err := marmotedu.NewForConfig(config)
if err != nil {
panic(err.Error())
}
request := &ladon.Request{
Resource: "resources:articles:ladon-introduction",
Action: "delete",
Subject: "users:peter",
Context: ladon.Context{
"remoteIP": "192.168.0.5",
},
}
// Authorize the request
fmt.Println("Authorize request...")
ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("Authorize response: %s.\n", ret.ToString())
}
在上面的代码示例中,包含了下面的操作。
首先,调用 BuildConfigFromFlags 函数创建出SDK的配置实例config
接着,调用 marmotedu.NewForConfig(config) 创建了IAM项目的客户端 clientset ;
最后,调用以下代码请求 /v1/authz 接口执行资源授权请求:
ret, err := clientset.Iam().AuthzV1().Authz().Authorize(context.TODO(), request, metav1.AuthorizeOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("Authorize response: %s.\n", ret.ToString())
调用格式为项目客户端.应用客户端.服务客户端.资源名.接口 。
所以上面的代码通过创建项目级别的客户端、应用级别的客户端和服务级别的客户端来调用资源的API接口。接下来我们来看下如何创建这些客户端。
marmotedu-sdk-go客户端设计
在讲客户端创建之前,我们先来看下客户端的设计思路。
Go项目的组织方式是有层级的Project -> Application -> Service。marmotedu-sdk-go很好地体现了这种层级关系使得SDK的调用更加易懂、易用。marmotedu-sdk-go的层级关系如下图所示
marmotedu-sdk-go定义了3类接口分别代表了项目、应用和服务级别的API接口
// 项目级别的接口
type Interface interface {
Iam() iam.IamInterface
Tms() tms.TmsInterface
}
// 应用级别的接口
type IamInterface interface {
APIV1() apiv1.APIV1Interface
AuthzV1() authzv1.AuthzV1Interface
}
// 服务级别的接口
type APIV1Interface interface {
RESTClient() rest.Interface
SecretsGetter
UsersGetter
PoliciesGetter
}
// 资源级别的客户端
type SecretsGetter interface {
Secrets() SecretInterface
}
// 资源的接口定义
type SecretInterface interface {
Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error)
Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error)
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error)
List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error)
SecretExpansion
}
Interface 代表了项目级别的接口,里面包含了 Iam 和 Tms 两个应用; IamInterface 代表了应用级别的接口里面包含了apiiam-apiserver和authziam-authz-server两个服务级别的接口。api和authz服务中又包含了各自服务中REST资源的CURD接口。
marmotedu-sdk-go通过 XxxV1 这种命名方式来支持不同版本的API接口好处是可以在程序中同时调用同一个API接口的不同版本例如
clientset.Iam().AuthzV1().Authz().Authorize() 、clientset.Iam().AuthzV2().Authz().Authorize() 分别调用了 /v1/authz 和 /v2/authz 两个版本的API接口。
上述关系也可以从目录结构中反映出来marmotedu-sdk-go目录设计如下只列出了一些重要的文件
├── examples # 存放SDK的使用示例
├── Makefile # 管理SDK源码静态代码检查、代码格式化、测试、添加版权信息等
├── marmotedu
│ ├── clientset.go # clientset实现clientset中包含多个应用多个服务的API接口
│ ├── fake # clientset的fake实现主要用于单元测试
│ └── service # 按应用进行分类存放应用中各服务API接口的具体实现
│ ├── iam # iam应用的API接口实现包含多个服务
│ │ ├── apiserver # iam应用中apiserver服务的API接口包含多个版本
│ │ │ └── v1 # apiserver v1版本API接口
│ │ ├── authz # iam应用中authz服务的API接口
│ │ │ └── v1 # authz服务v1版本接口
│ │ └── iam_client.go # iam应用的客户端包含了apiserver和authz 2个服务的客户端
│ └── tms # tms应用的API接口实现
├── pkg # 存放一些共享包,可对外暴露
├── rest # HTTP请求的底层实现
├── third_party # 存放修改过的第三方包例如gorequest
└── tools
└── clientcmd # 一些函数用来帮助创建rest.Config配置
每种类型的客户端,都可以通过以下相似的方式来创建:
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := xxxx.NewForConfig(config)
/root/.iam/config 为配置文件里面包含了服务的地址和认证信息。BuildConfigFromFlags 函数加载配置文件,创建并返回 rest.Config 类型的配置变量,并通过 xxxx.NewForConfig 函数创建需要的客户端。xxxx 是所在层级的client包例如 iam、tms。
marmotedu-sdk-go客户端定义了3类接口这可以带来两个好处。
第一API接口调用格式规范层次清晰可以使API接口调用更加清晰易记。
第二可以根据需要自行选择客户端类型调用灵活。举个例子在A服务中需要同时用到iam-apiserver 和 iam-authz-server提供的接口就可以创建应用级别的客户端IamClient然后通过 iamclient.APIV1() 和 iamclient.AuthzV1() 来切换调用不同服务的API接口。
接下来,我们来看看如何创建三个不同级别的客户端。
项目级别客户端创建
Interface 对应的客户端实现为Clientset所在的包为 marmotedu-sdk-go/marmoteduClientset客户端的创建方式为
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
clientset, err := marmotedu.NewForConfig(config)
调用方式为 clientset.应用.服务.资源名.接口 ,例如:
rsp, err := clientset.Iam().AuthzV1().Authz().Authorize()
参考示例为 marmotedu-sdk-go/examples/authz_clientset/main.go。
应用级别客户端创建
IamInterface 对应的客户端实现为IamClient所在的包为 marmotedu-sdk-go/marmotedu/service/iamIamClient客户端的创建方式为
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
iamclient,, err := iam.NewForConfig(config)
调用方式为 iamclient.服务.资源名.接口 ,例如:
rsp, err := iamclient.AuthzV1().Authz().Authorize()
参考示例为 marmotedu-sdk-go/examples/authz_iam/main.go。
服务级别客户端创建
AuthzV1Interface 对应的客户端实现为AuthzV1Client所在的包为 marmotedu-sdk-go/marmotedu/service/iam/authz/v1AuthzV1Client客户端的创建方式为
config, err := clientcmd.BuildConfigFromFlags("", "/root/.iam/config")
client, err := v1.NewForConfig(config)
调用方式为 client.资源名.接口 ,例如:
rsp, err := client.Authz().Authorize()
参考示例为 marmotedu-sdk-go/examples/authz/main.go。
上面我介绍了marmotedu-sdk-go的客户端创建方法接下来我们再来看下这些客户端具体是如何执行REST API请求的。
marmotedu-sdk-go的实现
marmotedu-sdk-go的实现和medu-sdk-go一样也是采用分层结构分为API层和基础层。如下图所示
RESTClient是整个SDK的核心RESTClient向下通过调用Request模块来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用gorequest包提供的方法完成HTTP的POST、PUT、GET、DELETE等请求获取HTTP返回结果并解析到指定的结构体中。RESTClient向上提供 Post() 、 Put() 、 Get() 、 Delete() 等方法来供客户端完成HTTP请求。
marmotedu-sdk-go提供了两类客户端分别是RESTClient客户端和基于RESTClient封装的客户端。
RESTClientRaw类型的客户端可以通过指定HTTP的请求方法、请求路径、请求参数等信息直接发送HTTP请求例如 client.Get().AbsPath("/version").Do().Into() 。
基于RESTClient封装的客户端例如AuthzV1Client、APIV1Client等执行特定REST资源、特定API接口的请求方便开发者调用。
接下来我们具体看下如何创建RESTClient客户端以及Request模块的实现。
RESTClient客户端实现
我通过下面两个步骤实现了RESTClient客户端。
第一步创建rest.Config类型的变量。
BuildConfigFromFlags函数通过加载yaml格式的配置文件来创建 rest.Config 类型的变量加载的yaml格式配置文件内容为
apiVersion: v1
user:
#token: # JWT Token
username: admin # iam 用户名
password: Admin@2020 # iam 密码
#secret-id: # 密钥 ID
#secret-key: # 密钥 Key
client-certificate: /home/colin/.iam/cert/admin.pem # 用于 TLS 的客户端证书文件路径
client-key: /home/colin/.iam/cert/admin-key.pem # 用于 TLS 的客户端 key 文件路径
#client-certificate-data:
#client-key-data:
server:
address: https://127.0.0.1:8443 # iam api-server 地址
timeout: 10s # 请求 api-server 超时时间
#max-retries: # 最大重试次数,默认为 0
#retry-interval: # 重试间隔,默认为 1s
#tls-server-name: # TLS 服务器名称
#insecure-skip-tls-verify: # 设置为 true 表示跳过 TLS 安全验证模式,将使得 HTTPS 连接不安全
certificate-authority: /home/colin/.iam/cert/ca.pem # 用于 CA 授权的 cert 文件路径
#certificate-authority-data:
在配置文件中,我们可以指定服务的地址、用户名/密码、密钥、TLS证书、超时时间、重试次数等信息。
创建方法如下:
config, err := clientcmd.BuildConfigFromFlags("", *iamconfig)
if err != nil {
panic(err.Error())
}
这里的代码中,*iamconfig 是yaml格式的配置文件路径。BuildConfigFromFlags 函数中调用LoadFromFile函数来解析yaml配置文件。LoadFromFile最终是通过 yaml.Unmarshal 的方式来解析yaml格式的配置文件的。
第二步根据rest.Config类型的变量创建RESTClient客户端。
通过RESTClientFor函数来创建RESTClient客户端
func RESTClientFor(config *Config) (*RESTClient, error) {
...
baseURL, versionedAPIPath, err := defaultServerURLFor(config)
if err != nil {
return nil, err
}
// Get the TLS options for this client config
tlsConfig, err := TLSConfigFor(config)
if err != nil {
return nil, err
}
// Only retry when get a server side error.
client := gorequest.New().TLSClientConfig(tlsConfig).Timeout(config.Timeout).
Retry(config.MaxRetries, config.RetryInterval, http.StatusInternalServerError)
// NOTICE: must set DoNotClearSuperAgent to true, or the client will clean header befor http.Do
client.DoNotClearSuperAgent = true
...
clientContent := ClientContentConfig{
Username: config.Username,
Password: config.Password,
SecretID: config.SecretID,
SecretKey: config.SecretKey,
...
}
return NewRESTClient(baseURL, versionedAPIPath, clientContent, client)
}
RESTClientFor函数调用defaultServerURLFor(config)生成基本的HTTP请求路径baseURL=http://127.0.0.1:8080versionedAPIPath=/v1。然后通过[TLSConfigFor](https://github.com/marmotedu/marmotedu-sdk-go/blob/v1.0.2/rest/config.go#L241-L298)函数生成TLS配置并调用 gorequest.New() 创建gorequest客户端将客户端配置信息保存在变量中。最后调用NewRESTClient函数创建RESTClient客户端。
RESTClient客户端提供了以下方法来供调用者完成HTTP请求
func (c *RESTClient) APIVersion() scheme.GroupVersion
func (c *RESTClient) Delete() *Request
func (c *RESTClient) Get() *Request
func (c *RESTClient) Post() *Request
func (c *RESTClient) Put() *Request
func (c *RESTClient) Verb(verb string) *Request
可以看到RESTClient提供了 Delete 、 Get 、 Post 、 Put 方法分别用来执行HTTP的DELETE、GET、POST、PUT方法提供的 Verb 方法可以灵活地指定HTTP方法。这些方法都返回了 Request 类型的变量。Request 类型的变量提供了一些方法用来完成具体的HTTP请求例如
type Response struct {
Allowed bool `json:"allowed"`
Denied bool `json:"denied,omitempty"`
Reason string `json:"reason,omitempty"`
Error string `json:"error,omitempty"`
}
func (c *authz) Authorize(ctx context.Context, request *ladon.Request, opts metav1.AuthorizeOptions) (result *Response, err error) {
result = &Response{}
err = c.client.Post().
Resource("authz").
VersionedParams(opts).
Body(request).
Do(ctx).
Into(result)
return
}
上面的代码中, c.client 是RESTClient客户端通过调用RESTClient客户端的 Post 方法,返回了 *Request 类型的变量。
*Request 类型的变量提供了 Resource 和 VersionedParams 方法来构建请求HTTP URL中的路径 /v1/authz ;通过 Body 方法指定了HTTP请求的Body。
到这里我们分别构建了HTTP请求需要的参数HTTP Method、请求URL、请求Body。所以之后就可以调用 Do 方法来执行HTTP请求并将返回结果通过 Into 方法保存在传入的result变量中。
Request模块实现
RESTClient客户端的方法会返回Request类型的变量Request类型的变量提供了一系列的方法用来构建HTTP请求参数并执行HTTP请求。
所以Request模块可以理解为最底层的通信层我们来看下Request模块具体是如何完成HTTP请求的。
我们先来看下Request结构体的定义
type RESTClient struct {
// base is the root URL for all invocations of the client
base *url.URL
// group stand for the client group, eg: iam.api, iam.authz
group string
// versionedAPIPath is a path segment connecting the base URL to the resource root
versionedAPIPath string
// content describes how a RESTClient encodes and decodes responses.
content ClientContentConfig
Client *gorequest.SuperAgent
}
type Request struct {
c *RESTClient
timeout time.Duration
// generic components accessible via method setters
verb string
pathPrefix string
subpath string
params url.Values
headers http.Header
// structural elements of the request that are part of the IAM API conventions
// namespace string
// namespaceSet bool
resource string
resourceName string
subresource string
// output
err error
body interface{}
}
再来看下Request结构体提供的方法
func (r *Request) AbsPath(segments ...string) *Request
func (r *Request) Body(obj interface{}) *Request
func (r *Request) Do(ctx context.Context) Result
func (r *Request) Name(resourceName string) *Request
func (r *Request) Param(paramName, s string) *Request
func (r *Request) Prefix(segments ...string) *Request
func (r *Request) RequestURI(uri string) *Request
func (r *Request) Resource(resource string) *Request
func (r *Request) SetHeader(key string, values ...string) *Request
func (r *Request) SubResource(subresources ...string) *Request
func (r *Request) Suffix(segments ...string) *Request
func (r *Request) Timeout(d time.Duration) *Request
func (r *Request) URL() *url.URL
func (r *Request) Verb(verb string) *Request
func (r *Request) VersionedParams(v interface{}) *Request
通过Request结构体的定义和使用方法我们不难猜测出Request模块通过 Name 、 Resource 、 Body 、 SetHeader 等方法来设置Request结构体中的各个字段。这些字段最终用来构建出一个HTTP请求并通过 Do 方法来执行HTTP请求。
那么如何构建并执行一个HTTP请求呢我们可以通过以下5步来构建并执行HTTP请求
构建HTTP URL
构建HTTP Method
构建HTTP Body
执行HTTP请求
保存HTTP返回结果。
接下来我们就来具体看下Request模块是如何构建这些请求参数并发送HTTP请求的。
第一步构建HTTP URL。
首先通过defaultServerURLFor函数返回了http://iam.api.marmotedu.com:8080 和 /v1 并将二者分别保存在了Request类型结构体变量中 c 字段的 base 字段和 versionedAPIPath 字段中。
通过 Do 方法执行HTTP时会调用r.URL()方法来构建请求URL。 r.URL 方法中通过以下代码段构建了HTTP请求URL
func (r *Request) URL() *url.URL {
p := r.pathPrefix
if len(r.resource) != 0 {
p = path.Join(p, strings.ToLower(r.resource))
}
if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 {
p = path.Join(p, r.resourceName, r.subresource, r.subpath)
}
finalURL := &url.URL{}
if r.c.base != nil {
*finalURL = *r.c.bas
}
finalURL.Path = p
...
}
p := r.pathPrefix 和 r.c.base ,是通过 defaultServerURLFor 调用返回的 v1 和 http://iam.api.marmotedu.com:8080 来构建的。
resourceName 通过 func (r *Request) Resource(resource string) *Request 来指定,例如 authz 。
所以最终我们构建的请求URL为 http://iam.api.marmotedu.com:8080/v1/authz 。
第二步构建HTTP Method。
HTTP Method通过RESTClient提供的 Post 、Delete 、Get 等方法来设置,例如:
func (c *RESTClient) Post() *Request {
return c.Verb("POST")
}
func (c *RESTClient) Verb(verb string) *Request {
return NewRequest(c).Verb(verb)
}
NewRequest(c).Verb(verb) 最终设置了Request结构体的 verb 字段,供 Do 方法使用。
第三步构建HTTP Body。
HTTP Body通过Request结构体提供的Body方法来指定
func (r *Request) Body(obj interface{}) *Request {
if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {
r.SetHeader("Content-Type", r.c.content.ContentType)
}
r.body = obj
return r
}
第四步执行HTTP请求。
通过Request结构体提供的Do方法来执行具体的HTTP请求代码如下
func (r *Request) Do(ctx context.Context) Result {
client := r.c.Client
client.Header = r.headers
if r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
client.WithContext(ctx)
resp, body, errs := client.CustomMethod(r.verb, r.URL().String()).Send(r.body).EndBytes()
if err := combineErr(resp, body, errs); err != nil {
return Result{
response: &resp,
err: err,
body: body,
}
}
decoder, err := r.c.content.Negotiator.Decoder()
if err != nil {
return Result{
response: &resp,
err: err,
body: body,
decoder: decoder,
}
}
return Result{
response: &resp,
body: body,
decoder: decoder,
}
}
在Do方法中使用了Request结构体变量中各个字段的值通过 client.CustomMethod 来执行HTTP请求。 client 是 *gorequest.SuperAgent 类型的客户端。
第五步保存HTTP返回结果。
通过Request结构体的 Into 方法来保存HTTP返回结果
func (r Result) Into(v interface{}) error {
if r.err != nil {
return r.Error()
}
if r.decoder == nil {
return fmt.Errorf("serializer doesn't exist")
}
if err := r.decoder.Decode(r.body, &v); err != nil {
return err
}
return nil
}
r.body 是在Do方法中执行完HTTP请求后设置的它的值为HTTP请求返回的Body。
请求认证
接下来我再来介绍下marmotedu-sdk-go另外一个比较核心的功能请求认证。
marmotedu-sdk-go支持两种认证方式
Basic认证通过给请求添加 Authorization: Basic xxxx 来实现。
Bearer认证通过给请求添加 Authorization: Bearer xxxx 来实现。这种方式又支持直接指定JWT Token或者通过指定密钥对由SDK自动生成JWT Token。
Basic认证和Bearer认证我在 25讲介绍过你可以返回查看下。
认证头是RESTClient客户端发送HTTP请求时指定的具体实现位于NewRequest函数中
switch {
case c.content.HasTokenAuth():
r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.content.BearerToken))
case c.content.HasKeyAuth():
tokenString := auth.Sign(c.content.SecretID, c.content.SecretKey, "marmotedu-sdk-go", c.group+".marmotedu.com")
r.SetHeader("Authorization", fmt.Sprintf("Bearer %s", tokenString))
case c.content.HasBasicAuth():
// TODO: get token and set header
r.SetHeader("Authorization", "Basic "+basicAuth(c.content.Username, c.content.Password))
}
上面的代码会根据配置信息,自动判断使用哪种认证方式。
总结
这一讲中我介绍了Kubernetes client-go风格的SDK实现方式。和公有云厂商的SDK设计相比client-go风格的SDK设计有很多优点。
marmotedu-sdk-go在设计时通过接口实现了3类客户端分别是项目级别的客户端、应用级别的客户端和服务级别的客户端。开发人员可以根据需要自行创建客户端类型。
marmotedu-sdk-go通过RESTClientFor创建了RESTClient类型的客户端RESTClient向下通过调用Request模块来完成HTTP请求方法、请求路径、请求体、认证信息的构建。Request模块最终通过调用gorequest包提供的方法完成HTTP的POST、PUT、GET、DELETE等请求获取HTTP返回结果并解析到指定的结构体中。RESTClient向上提供 Post() 、 Put() 、 Get() 、 Delete() 等方法来供客户端完成HTTP请求。
课后练习
阅读defaultServerURLFor源码思考下defaultServerURLFor是如何构建请求地址 http://iam.api.marmotedu.com:8080 和API版本 /v1 的。
使用gorequest包编写一个可以执行以下HTTP请求的示例
curl -XPOST http://example.com/v1/user -d '{"username":"colin","address":"shenzhen"}'
欢迎你在留言区与我交流讨论,我们下一讲见。