<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Traefik Plugins 全面解析

          共 25131字,需瀏覽 51分鐘

           ·

          2021-07-28 09:13

          介紹

          前置知識:Traefik 使用指北

          Traefik v2.3 及以上版本允許開發(fā)人員使用 Plugins 插件向 Traefik 添加新功能或定義新行為。例如,可以修改請求或標頭、重定向、添加身份驗證等,提供與 Traefik 中間件類似的功能。

          不過,和傳統(tǒng)中間件不同,插件是動態(tài)加載的,并由嵌入式解釋器執(zhí)行,無需編譯二進制文件,所有插件都是 100% 跨平臺的,這使得它們易于開發(fā)和共享(通過 Traefik Pilot)。

          Traefik Pilot

          Traefik Pilot 是一個 Traefik 的監(jiān)控和管理平臺,可以集中管理在任何環(huán)境中運行的所有 Traefik 實例。它通過統(tǒng)一的儀表板提供對 Traefik 實例的觀察性和控制,可提供詳細的網(wǎng)絡(luò)指標、服務(wù)器監(jiān)控和安全通知。

          Traefik Pilot 還為自定義中間件插件托管了一個公共插件中心(public plugins hub),支持流量整形、流量 QoS、流量速率限制等。

          在使用 Plugins 之前,需要在 Traefik Pilot 平臺(https://pilot.traefik.io/)上注冊一個賬號,這里我直接使用 Github 授權(quán)登陸:

          登陸成功后,我們需要注冊一個 Traefik Instance ,點擊 Register New Traefik Instance 按鈕會生成一個 token ,復(fù)制 token :

          勾選 I have restarted my Traefik instance 保存該 Traefik Instance :

          此時,顯示還未綁定我們的 Trarfik 實例。

          綁定 Traefik Instance

          創(chuàng)建 traefik 配置 traefik-config.yaml ,并填入上面得到的 token :

          pilot:
            enabled: true
            token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
            dashboard: true
          ports:
            traefik:
              expose: true
            web:
              nodePort: 80
            websecure:
              nodePort: 443

          使用 helm 安裝 :

          helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

          訪問面板,可以看到,Traefik Instance 已經(jīng)綁定了我們的 traefik 實例:

          可以通過 Metrics 觀察指標:


          Traefik Plugins 使用

          以 plugindemo 插件為例,為請求頭部添加一個 whoami-header: hello world

          修改 traefik 配置 traefik-config.yaml ,開啟并安裝 [email protected] 插件:

          pilot:
            enabled: true
            token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
            dashboard: true
          additionalArguments:
            - "--experimental.plugins.plugindemo.modulename=github.com/traefik/plugindemo"
            - "--experimental.plugins.plugindemo.version=v0.2.1"
          experimental:
            plugins:
              enabled: true
          ports:
            traefik:
              expose: true
            web:
              nodePort: 80
            websecure:
              nodePort: 443

          使用 helm 更新重啟 traefik :

          helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

          創(chuàng)建 whoami.yaml

          apiVersion: traefik.containo.us/v1alpha1
          kind: Middleware
          metadata:
            name: whoamiplugin
          spec:
            plugin:
              plugindemo:  # plugindemo 插件
                Headers:
                  whoami-header: hello world
          ---
          apiVersion: traefik.containo.us/v1alpha1
          kind: IngressRoute
          metadata:
            name: whoamiingressroute # 入口路由名稱
          spec:
            entryPoints: # 網(wǎng)絡(luò)入口點
              - web
            routes:
            - match: Host(`master`) && PathPrefix(`/whoami/`) # 路由匹配器,匹配 http://master/whoami/
              middlewares: # 使用 plugindemo 插件
              - name: whoamiplugin
              kind: Rule
              services: # 代理服務(wù)
              - name: whoami
                port: 80
          ---
          apiVersion: v1
          kind: Service
          metadata:
            name: whoami
          spec:
            ports:
              - protocol: TCP
                name: web
                port: 80
            selector:
              app: whoami
          ---
          kind: Deployment
          apiVersion: apps/v1
          metadata:
            name: whoami
            labels:
              app: whoami
          spec:
            replicas: 2
            selector:
              matchLabels:
                app: whoami
            template:
              metadata:
                labels:
                  app: whoami
              spec:
                containers:
                  - name: whoami
                    image: containous/whoami
                    ports:
                      - name: web
                        containerPort: 80

          使用 kubectl 啟動 whoami :

          kubectl apply -f whoami.yaml

          查看插件使用效果:

          請求 header 已增加 whoami-header: hello world

          Traefik Plugins 源碼分析

          源碼以 https://github.com/traefik/traefik/commit/ca2ff214c49a2aa1f8b590d4f2158f0ea734322b 版本為例。

          traefik 會在 cmd/traefik/traefik.go 的 172 行 setupServer 函數(shù)進行初始化服務(wù),其中構(gòu)造插件實例也是在此函數(shù)內(nèi)完成:

           // 構(gòu)造插件實例
           pluginBuilder, err := createPluginBuilder(staticConfiguration)
           if err != nil {
            return nil, err
           }

          深入 createPluginBuilder 函數(shù):

          func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) {
              // 初始化插件
           client, plgs, localPlgs, err := initPlugins(staticConfiguration)
           if err != nil {
            return nil, err
           }
           // 返回 *plugins.Builder ,實現(xiàn)了 PluginsBuilder 接口
           return plugins.NewBuilder(client, plgs, localPlgs)
          }

          initPlugins 過程中,會進行插件配置檢查,下載,解壓等一系列操作:

          func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) {
           err := checkUniquePluginNames(staticCfg.Experimental)
           if err != nil {
            return nilnilnil, err
           }

           var client *plugins.Client
           plgs := map[string]plugins.Descriptor{}

           // 是否啟用了 Traefik Pilot,并且配置了插件
           if isPilotEnabled(staticCfg) && hasPlugins(staticCfg) {
            opts := plugins.ClientOptions{
             Output: outputDir,
             Token:  staticCfg.Pilot.Token, // Pilot 的 token
            }

            var err error
            // 創(chuàng)建插件客戶端
            client, err = plugins.NewClient(opts)
            if err != nil {
             return nilnilnil, err
            }

            // 初始化所有插件
            err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins)
            if err != nil {
             return nilnilnil, err
            }

            plgs = staticCfg.Experimental.Plugins
           }

           localPlgs := map[string]plugins.LocalDescriptor{}

           if hasLocalPlugins(staticCfg) {
            err := plugins.SetupLocalPlugins(staticCfg.Experimental.LocalPlugins)
            if err != nil {
             return nilnilnil, err
            }

            localPlgs = staticCfg.Experimental.LocalPlugins
           }

           return client, plgs, localPlgs, nil
          }

          其中下載插件的邏輯如下:

          func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error {
           // 檢查插件配置
           err := checkRemotePluginsConfiguration(plugins)
           if err != nil {
            return fmt.Errorf("invalid configuration: %w", err)
           }
           // 清理舊插件
           err = client.CleanArchives(plugins)
           if err != nil {
            return fmt.Errorf("failed to clean archives: %w", err)
           }

           ctx := context.Background()

           // 依次下載所有插件
           for pAlias, desc := range plugins {
            log.FromContext(ctx).Debugf("loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version)

            // 開始下載插件
            hash, err := client.Download(ctx, desc.ModuleName, desc.Version)
            if err != nil {
             _ = client.ResetAll()
             return fmt.Errorf("failed to download plugin %s: %w", desc.ModuleName, err)
            }

            // hash 校驗
            err = client.Check(ctx, desc.ModuleName, desc.Version, hash)
            if err != nil {
             _ = client.ResetAll()
             return fmt.Errorf("failed to check archive integrity of the plugin %s: %w", desc.ModuleName, err)
            }
           }

           err = client.WriteState(plugins)
           if err != nil {
            _ = client.ResetAll()
            return fmt.Errorf("failed to write plugins state: %w", err)
           }

           // 解壓所有下載成功的插件
           for _, desc := range plugins {
            err = client.Unzip(desc.ModuleName, desc.Version)
            if err != nil {
             _ = client.ResetAll()
             return fmt.Errorf("failed to unzip archive: %w", err)
            }
           }

           return nil
          }

          分析 client.Download() 的關(guān)鍵源碼,可以知道,traefik 的插件下載 url 格式為 https://plugin.pilot.traefik.io/public/download/github.com/traefik/plugindemo/v0.2.1 :

          const pilotURL = "https://plugin.pilot.traefik.io/public/"
          ...
           // 組合 url , pilotURL/download/插件名/版本號
           endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion))
           if err != nil {
            return "", fmt.Errorf("failed to parse endpoint URL: %w", err)
           }

           req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
           if err != nil {
            return "", fmt.Errorf("failed to create request: %w", err)
           }

           if hash != "" {
            req.Header.Set(hashHeader, hash)
           }

           if c.token != "" {
            req.Header.Set(tokenHeader, c.token)
           }

           resp, err := c.HTTPClient.Do(req)
          ...

          可以使用 curl 工具驗證:

          curl -H 'X-Token:898bb869-77ad-4594-b68f-1f87e0aa2e9b' -O https://plugin.pilot.traefik.io/public/download/github.com/traefik/plugindemo/v0.2.1

          traefik 會將插件下載到 /plugins-storage 目錄。

          插件下載解壓完成后,會使用 plugins.NewBuilder(client, plgs, localPlgs) 將插件源碼讀取加載到 *plugins.Builder 實例,這里用到一個十分強大的 go 解釋器庫 yaegi ,同樣出自 traefik 之手,地址在 https://github.com/traefik/yaegi ,使用起來也很簡單,只要使用 new()創(chuàng)建解釋器,后續(xù)使用 Eval()就可以運行代碼了。

          traefik 插件分為 provider 和 middleware 兩種,故 *plugins.Builder 實例也提供了 middlewareDescriptors 和 providerDescriptors 兩種 map 類型來存放插件:

          // NewBuilder creates a new Builder.
          func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
           pb := &Builder{
                  middlewareDescriptors: map[string]pluginContext{}, // 中間件類型插件包名:插件實例
                  providerDescriptors:   map[string]pluginContext{}, // 提供者類型插件包名:插件實例
           }

           for pName, desc := range plugins {
            manifest, err := client.ReadManifest(desc.ModuleName)
            if err != nil {
             _ = client.ResetAll()
             return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
            }

            // 創(chuàng)建 go 解釋器
            i := interp.New(interp.Options{GoPath: client.GoPath()})

            err = i.Use(stdlib.Symbols)
            if err != nil {
             return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
            }

            err = i.Use(ppSymbols())
            if err != nil {
             return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
            }

            // 導入包
            _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
            if err != nil {
             return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
            }

            switch manifest.Type {
            case "middleware":
             // 將 middleware 類型插件放置到這里
             pb.middlewareDescriptors[pName] = pluginContext{
              interpreter: i, // 解釋器實例
              GoPath:      client.GoPath(),
              Import:      manifest.Import,
              BasePkg:     manifest.BasePkg,
             }
            case "provider":
             // 將 provider 類型插件放置到這里
             pb.providerDescriptors[pName] = pluginContext{
              interpreter: i, // 解釋器實例
              GoPath:      client.GoPath(),
              Import:      manifest.Import,
              BasePkg:     manifest.BasePkg,
             }
            default:
             return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
            }
           }
              ......

              // 返回 *plugins.Builder 實例
           return pb, nil
          }

          初始化構(gòu)造插件完成后回到 cmd/traefik/traefik.gosetupServer 函數(shù)中,會進行插件的動態(tài)加載過程,首先是 Provider 類型的插件:

           // Providers plugins

           // 遍歷 Provider 類型的插件
           for name, conf := range staticConfiguration.Providers.Plugin {
                  // 實例化插件
            p, err := pluginBuilder.BuildProvider(name, conf)
            if err != nil {
             return nil, fmt.Errorf("plugin: failed to build provider: %w", err)
            }

            err = providerAggregator.AddProvider(p)
            if err != nil {
             return nil, fmt.Errorf("plugin: failed to add provider: %w", err)
            }
           }

          traefik 插件規(guī)定必須實現(xiàn) CreateConfig 和 New 函數(shù),而 pluginBuilder.BuildProvider 就是使用解釋器執(zhí)行插件的 CreateConfig 函數(shù),然后使用 wrapper.NewWrapper 調(diào)用插件的 New 函數(shù):

           // 使用之前保存的解釋器去調(diào)用插件的 CreateConfig 函數(shù)
           vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`)
           if err != nil {
            return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
           }
           ......
           _, err = descriptor.interpreter.Eval(`package wrapper

          import (
           "context"

           `
           + basePkg + ` "` + descriptor.Import + `"
           "github.com/traefik/traefik/v2/pkg/plugins"
          )

          func NewWrapper(ctx context.Context, config *`
           + basePkg + `.Config, name string) (plugins.PP, error) {
           p, err := `
           + basePkg + `.New(ctx, config, name)
           var pv plugins.PP = p
           return pv, err
          }
          `
          )
           if err != nil {
            return nil, fmt.Errorf("failed to eval wrapper: %w", err)
           }

           fnNew, err := descriptor.interpreter.Eval("wrapper.NewWrapper")
           if err != nil {
            return nil, fmt.Errorf("failed to eval New: %w", err)
           }
          ...

          Provider 加載完成后,traefik 就會開始監(jiān)聽路由,若路由配置了中間件插件,traefik 就會同理去加載對應(yīng)的 middleware 類型插件:

           // Plugin
           if config.Plugin != nil {
            if middleware != nil {
             return nil, badConf
            }

            pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin)
            if err != nil {
             return nil, fmt.Errorf("plugin: %w", err)
            }

                  // 執(zhí)行中間件類型插件
            plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
            if err != nil {
             return nil, fmt.Errorf("plugin: %w", err)
            }

            middleware = func(next http.Handler) (http.Handler, error) {
             return plug(ctx, next)
            }
           }

          關(guān)鍵地方在 pkg/plugins/middlewares.go 的 40 行,同樣是使用解釋器執(zhí)行插件的 CreateConfig 和 New 函數(shù):

          func newMiddleware(descriptor pluginContext, config map[string]interface{}, middlewareName string) (*Middleware, error) {
           basePkg := descriptor.BasePkg
           if basePkg == "" {
            basePkg = strings.ReplaceAll(path.Base(descriptor.Import), "-""_")
           }

              // 使用之前保存的解釋器去調(diào)用插件的 CreateConfig 函數(shù)
           vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`)
           if err != nil {
            return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
           }

           cfg := &mapstructure.DecoderConfig{
            DecodeHook:       mapstructure.StringToSliceHookFunc(","),
            WeaklyTypedInput: true,
            Result:           vConfig.Interface(),
           }

           decoder, err := mapstructure.NewDecoder(cfg)
           if err != nil {
            return nil, fmt.Errorf("failed to create configuration decoder: %w", err)
           }

           err = decoder.Decode(config)
           if err != nil {
            return nil, fmt.Errorf("failed to decode configuration: %w", err)
           }

              // 使用之前保存的解釋器去調(diào)用插件的 New 函數(shù)
           fnNew, err := descriptor.interpreter.Eval(basePkg + `.New`)
           if err != nil {
            return nil, fmt.Errorf("failed to eval New: %w", err)
           }

           return &Middleware{
            middlewareName: middlewareName,
            fnNew:          fnNew,
            config:         vConfig,
           }, nil
          }


          traefik插件架構(gòu)

          Traefik Plugins 開發(fā)

          上文分析 traefik 的插件實現(xiàn)源碼已經(jīng)知道,traefik 的插件是靠 Yaegi 解釋器動態(tài)加載實現(xiàn)的,所以開發(fā) traefik 插件變得很簡單,和開發(fā) web 瀏覽器擴展一樣。

          traefik 的插件托管在 GitHub ,這里以 https://github.com/togettoyou/traefik-timer-plugin 為例。

          GitHub 倉庫需要按照規(guī)范,有 readme.md ,需設(shè)置一個名稱為 traefik-plugin 的 topic ,根目錄下需要創(chuàng)建一個 .traefik.yml 配置文件:

          # 在 Traefik Pilot Web UI 中顯示的插件的名稱
          displayName: Timer Plugin

          # 插件類型,目前版本只支持 middleware
          type: middleware

          # 插件導入路徑
          import: github.com/togettoyou/traefik-timer-plugin

          # 插件簡介
          summary: 用于請求響應(yīng)計時

          # 配置數(shù)據(jù)
          testData:
            log: true

          接下來就是開發(fā)代碼了,traefik 也為插件代碼提供了規(guī)范,需要包含以下對象:

          • type Config struct { ... } 結(jié)構(gòu)體,字段任意。
          • func CreateConfig() *Config 函數(shù)。
          • func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) 函數(shù)。

          代碼示例:

          package traefik_timer_plugin

          import (
           "context"
           "fmt"
           "net/http"
           "time"
          )

          // Config 自定義配置
          type Config struct {
           Log bool `json:"log,omitempty"`
          }

          // CreateConfig 提供給 traefik 設(shè)置配置
          func CreateConfig() *Config {
           return &Config{}
          }

          // New 提供給 traefik 創(chuàng)建 Timer 插件
          func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
           return &Timer{
            next: next,
            name: name,
            log:  config.Log,
           }, nil
          }

          type Timer struct {
           next http.Handler
           name string
           log  bool
          }

          func (t *Timer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
           start := time.Now()
           t.next.ServeHTTP(rw, req)
           cost := time.Since(start)
           if t.log {
            fmt.Println("請求花費時間:", cost)
           }
          }

          最后的最后,為倉庫打一個版本標簽如 v0.1.0 即可發(fā)布插件。


          插件驗證,修改 traefik 配置 traefik-config.yaml ,安裝 [email protected] 插件:

          pilot:
            enabled: true
            token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
            dashboard: true
          additionalArguments:
            - "--experimental.plugins.traefik_timer_plugin.modulename=github.com/togettoyou/traefik-timer-plugin"
            - "--experimental.plugins.traefik_timer_plugin.version=v0.1.0"
          experimental:
            plugins:
              enabled: true
          ports:
            traefik:
              expose: true
            web:
              nodePort: 80
            websecure:
              nodePort: 443

          使用 helm 更新重啟 traefik :

          helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

          更改 whoami.yaml

          apiVersion: traefik.containo.us/v1alpha1
          kind: Middleware
          metadata:
            name: timerplugin
          spec:
            plugin:
              traefik_timer_plugin:  # 自定義的計時插件
                log: true
          ---
          apiVersion: traefik.containo.us/v1alpha1
          kind: IngressRoute
          metadata:
            name: whoamiingressroute # 入口路由名稱
          spec:
            entryPoints: # 網(wǎng)絡(luò)入口點
              - web
            routes:
            - match: Host(`master`) && PathPrefix(`/whoami/`) # 路由匹配器,匹配 http://master/whoami/
              middlewares: # 使用 timerplugin 插件
              - name: timerplugin
              kind: Rule
              services: # 代理服務(wù)
              - name: whoami
                port: 80
          ---
          apiVersion: v1
          kind: Service
          metadata:
            name: whoami
          spec:
            ports:
              - protocol: TCP
                name: web
                port: 80
            selector:
              app: whoami
          ---
          kind: Deployment
          apiVersion: apps/v1
          metadata:
            name: whoami
            labels:
              app: whoami
          spec:
            replicas: 2
            selector:
              matchLabels:
                app: whoami
            template:
              metadata:
                labels:
                  app: whoami
              spec:
                containers:
                  - name: whoami
                    image: containous/whoami
                    ports:
                      - name: web
                        containerPort: 80

          重啟 whoami :

          kubectl apply -f whoami.yaml

          訪問路由并查看 traefik 日志:

          kubectl logs -f traefik-xxxxxx -n traefik

          當然,這里只是作為示例,traefik 的插件機制開發(fā)必然可以為我們提供更強大的功能。

          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看视频黄免费 | 啪啪啪免费在线观看 | 亚洲色图 15p | 国产成人精品一区二区三区四区五区 | 欧美三级成人理伦 |