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

          Kubevela VelaUX 的 Plugin 機(jī)制及實(shí)現(xiàn)原理

          共 19389字,需瀏覽 39分鐘

           ·

          2024-03-19 15:51

          作者:李艷芳,中國(guó)移動(dòng)云能力中心軟件研發(fā)工程師,專(zhuān)注于云原生、微服務(wù)、算力網(wǎng)絡(luò)等

          Kube Vela 是 個(gè)現(xiàn)代化應(yīng)用交付與管理平臺(tái)。 VelaUX 以KubeVela addon的形式存在為 KubeVela 提供了可 視化的 UI 控制臺(tái)操作能力,大大降低了KubeVela的使用門(mén)檻,使得用戶(hù)只需通過(guò)頁(yè)面上操作就可完成應(yīng)用交付與管理。 為了滿(mǎn)足用戶(hù)的各種不同需求,VelaUX同樣提供了一種擴(kuò)展機(jī)制,使得用戶(hù)可以定制化自己的UI控制臺(tái),就是Plugin機(jī)制。 本文介紹Plugin的開(kāi)發(fā)和實(shí)現(xiàn)原理。

          一、什么是Plugin機(jī)制

          簡(jiǎn)單來(lái)說(shuō),Plugin機(jī)制提供了一種框架用戶(hù)通過(guò)開(kāi)發(fā)自己的Plugin可以為VelaUX新增自定義頁(yè)面。如下圖,VelaUx本身是沒(méi)有節(jié)點(diǎn)管理這個(gè)頁(yè)面的,現(xiàn)在我們可以開(kāi)發(fā)一個(gè)Plugin,為VelaUX新增這樣一個(gè)頁(yè)面c168030f558327374f175639c9244f60.webp

          二、怎么開(kāi)發(fā)一個(gè)Plugin

          社區(qū)提供了Plugin的模版,我們可以從克隆一個(gè)Plugin模版開(kāi)始開(kāi)發(fā)。從地址 https://github.com/kubevela-contrib/velaux-plugin-template 克隆一個(gè)Plugin下來(lái),我們看到一個(gè)Plugin的目錄結(jié)構(gòu)如下:

                src
          asset
          components
          App
          index.less
          index.tsx
          PluginConfing
          index.ts
          module.ts
          plugin.json
          package.json

          其中:

          • plugin.json是Plugin的元數(shù)據(jù)如:Plugin的名字、id、描述信息以及其他相關(guān)信息。
          • module.ts中的內(nèi)容一般無(wú)需修改。開(kāi)發(fā)完成的Plugin作為js的一個(gè)模塊存在,通過(guò)模塊加載機(jī)制將Plugin頁(yè)面加載到VelaUX主控制臺(tái)中,module.ts就是該js模塊的入口,主要是定義了一個(gè)AppPagePlugin對(duì)象,VelaUX渲染Plugin的時(shí)候就是通過(guò)該對(duì)象獲取具體需要渲染頁(yè)面內(nèi)容。
          • components文件夾下App目錄和PluginConfig目錄分別用來(lái)編寫(xiě)新擴(kuò)展的頁(yè)面和其配置頁(yè)面,跟開(kāi)發(fā)普通的頁(yè)面沒(méi)有什么區(qū)別。繼續(xù)看App/index.tsx文件可以看到定義了一個(gè)App組件,該組件就是要新擴(kuò)展的頁(yè)面組件。

          在開(kāi)發(fā)頁(yè)面組件或頁(yè)面配置組件時(shí),如果需要調(diào)用Vela Apiserver本身的接口只需要通過(guò)getBackendSrv().get('/api/v1/clusters')方式調(diào)用即可:

                
                import { getBackendSrv } from '@velaux/ui';

          getBackendSrv().get('/api/v1/clusters').then(res=>{console.log(res)})

          想使用VelaUX中已經(jīng)寫(xiě)好的React組件也是像如下直接引用即可:

          import { Table, Form } from '@velaux/ui'

          完成Plugin開(kāi)發(fā)和build后只需要在啟動(dòng)VelaUX的命令后通過(guò)--plugin-path參數(shù)制定插件等位置,新擴(kuò)展的頁(yè)面就顯示到VelaUX控制臺(tái)中了。

          三、VelaUX Apiserver中本身提供的接口不夠用怎么辦

          有時(shí)候我們會(huì)發(fā)現(xiàn)需要使用的接口VelaUX并沒(méi)有提供,比如實(shí)現(xiàn)一個(gè)對(duì)集群的監(jiān)控頁(yè)面需要調(diào)用K8S本身的API接口,VelaUX本身的API是沒(méi)有提供的,這時(shí)就需要借助VelaUX的Plugin機(jī)制。Plugin機(jī)制內(nèi)部通過(guò)反向代理可以將接口轉(zhuǎn)發(fā)至需要的K8S Apiserver或者自定義的服務(wù)上。

          如果需要新的API支持,開(kāi)發(fā)Plugin的時(shí)候需要修改Plugin元數(shù)據(jù),即在plugin.json文件中添加"backend"、"backendType"字段。backend設(shè)置為true代表需要后端接口支持。backendType用來(lái)指定API的類(lèi)型,有兩個(gè)取值:"kube-api"和"kube-service",分別代表將請(qǐng)求轉(zhuǎn)發(fā)至K8S Apiserver上和自定義的服務(wù)上。接口調(diào)用時(shí)也需要在路徑前加上"/proxy/plugins/${pluginID}",如下所示:

                
                getBackendSrv().get(`/proxy/plugins/${pluginID}/${realPath}`).then(res=>{console.log(res)})

          通過(guò)查看VelaUX的啟動(dòng)過(guò)程可以發(fā)現(xiàn),如果請(qǐng)求接口的路徑前綴是"/proxy/plugins/",VelaUX為其進(jìn)行了特殊處理-通過(guò)proxyPluginBackend方法進(jìn)行處理

                
                func (s *restServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
              ....
              switch {
              case strings.HasPrefix(req.URL.Path, "/proxy/plugins/"):
                  utils.NewFilterChain(s.proxyPluginBackend, api.AuthTokenCheck, api.AuthUserCheck(s.UserService)).ProcessFilter(req, res)
                  return

          proxyPluginBackend方法通過(guò)調(diào)用router.GetPluginHandler注冊(cè)了Plugin的plugin.json中配置的路由規(guī)則,并交由pluginBackendProxyHandler處理

                
                func (s *restServer) proxyPluginBackend(req *http.Request, res http.ResponseWriter) {
              plugin, err := s.PluginService.GetPlugin(req.Context(), pluginID)
              // Register the plugin route
              router.GetPluginHandler(plugin, s.pluginBackendProxyHandler).ServeHTTP(res, req)
          }

          pluginBackendProxyHandler中新建了一個(gè)PluginProxy對(duì)象pro, 并由該代理對(duì)象處理請(qǐng)求

                
                func (s *restServer) pluginBackendProxyHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params, plugin *plugintypes.Plugin, route *plugintypes.Route) {
              ...
              pro, err := proxy.NewBackendPluginProxy(plugin, s.KubeClient, s.KubeConfig)
              ...
              
              r.URL.Path = strings.Replace(r.URL.Path, "/proxy/plugins/"+plugin.PluginID(), ""1)
              r = r.WithContext(context.WithValue(r.Context(), &proxy.RouteCtxKey, route))
              pro.Handler(r, w)
          }

          至此能看到所有路徑以"/proxy/plugins/"開(kāi)頭的請(qǐng)求,VelaUX都為其新建了代理,通過(guò)代理轉(zhuǎn)發(fā)到相應(yīng)的RESTFull服務(wù)上。

          從NewBackendPluginProxy方法中可以看到VelaUX根據(jù)plugin的BackendType字段創(chuàng)建對(duì)應(yīng)類(lèi)型的代理。Plugin機(jī)制目前實(shí)現(xiàn)了兩種類(lèi)型的代理:KubeAPI類(lèi)型和KubeService類(lèi)型。KubeAPI類(lèi)型的代理可以將請(qǐng)求轉(zhuǎn)發(fā)至K8S Apiserver上,KubeService類(lèi)型代理可以將請(qǐng)求轉(zhuǎn)發(fā)至自定義服務(wù)上。

                
                func NewBackendPluginProxy(plugin *types.Plugin, kubeClient client.Client, kubeConfig *rest.Config) (BackendProxy, error) {
              p, ok := proxyCache[plugin]
              switch plugin.BackendType {
                  case types.KubeAPI:
                      p, err = NewKubeAPIProxy(kubeConfig, plugin)
                      if err != nil {
                          return nil, err
                      }
                  case types.KubeService:
                      p = NewKubeServiceProxy(kubeClient, plugin)
                  default:
                      return nil, ErrAvailablePlugin
              }
              proxyCache[plugin] = p
              return p, nil
          }

          繼續(xù)查看KubeServiceProxy的Handler方法發(fā)現(xiàn),VelaUX通過(guò)kubeClient去集群上查找指定NameSpace下端口為指定端口的Service服務(wù)。該Service服務(wù)的服務(wù)地址http://ClusterIP:Port就是請(qǐng)求將要被轉(zhuǎn)發(fā)到的目的地址,保存在變量k.availableEndpoint中。

                
                        var service corev1.Service
                  namespace := k.plugin.BackendService.Namespace
                  name := k.plugin.BackendService.Name
                  if namespace == "" {
                      namespace = kubevelatypes.DefaultKubeVelaNS
                  }
                  err := k.kubeClient.Get(req.Context(), apitypes.NamespacedName{Namespace: namespace, Name: name}, &service); err != nil {
                     
                  matchPort := service.Spec.Ports[0].Port
                  if k.plugin.BackendService.Port != 0 {
                      havePort := false
                      for _, port := range service.Spec.Ports {
                          if k.plugin.BackendService.Port == port.Port {
                              havePort = true
                              matchPort = k.plugin.BackendService.Port
                              break
                          }
                      }
                  }

                  availableEndpoint, err := url.Parse(fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, matchPort))
                  if err != nil {
                      bcode.ReturnHTTPError(req, res, bcode.ErrNotFound)
                  }
                  k.availableEndpoint = availableEndpoint

          接下來(lái)就是以k.availableEndpoint為目標(biāo)地址新建一個(gè)反向代理,這樣該P(yáng)lugin相應(yīng)的接口就都轉(zhuǎn)發(fā)到了所指定的NameSpace下的端口為指定端口的Service上。

                
                    director := func(req *http.Request) {
                  var base = *k.availableEndpoint
                  base.Path = req.URL.Path
                  req.URL = &base
                  if route != nil {
                      // Setting the custom proxy headers
                      for _, h := range route.ProxyHeaders {
                          req.Header.Set(h.Name, h.Value)
                      }
                  }
                  // Setting the authentication
                  if types.Basic == k.plugin.AuthType && k.plugin.AuthSecret != nil {
                      if err := k.setBasicAuth(req); err != nil {
                          klog.Errorf("can't set the basic auth, err:%s", err.Error())
                          return
                      }
                  }
                  for k, v := range req.URL.Query() {
                      for _, v1 := range v {
                          base.Query().Add(k, v1)
                      }
                  }
              }
              rp := &httputil.ReverseProxy{Director: director, ErrorLog: log.Default()}
              rp.ServeHTTP(res, req)
          }

          KubeAPIProxy實(shí)現(xiàn)類(lèi)似,這里不再贅述。

          四、Plugin的加載過(guò)程

          總的來(lái)說(shuō)Plugin的加載過(guò)程就是:

          1、就是從先從指定目錄下遍歷查找并讀取plugin.json文件內(nèi)容,并創(chuàng)建對(duì)應(yīng)的plugin對(duì)象

          2、判斷是否是需要KubeAPI類(lèi)型的后端支持,如果是就為其創(chuàng)建對(duì)應(yīng)的ClusterRole/ClusterRoleBinding資源

          下面就是加載plugin的代碼,p.loader.Load一行完成了plugin的加載和plugin對(duì)象的創(chuàng)建,range循環(huán)部分通過(guò)判斷plugin類(lèi)型,按需初始化plugin角色,其實(shí)就是創(chuàng)建對(duì)應(yīng)的ClusterRole/ClusterRoleBinding資源對(duì)象。

                
                func (p *pluginImpl) LoadNewPlugin(ctx context.Context, s types.PluginSource) error {
              plugins, err := p.loader.Load(s.Class, s.Paths, nil)

              for _, plugin := range plugins {
                  if plugin.BackendType == types.KubeAPI && len(plugin.KubePermissions) > 0 {
                      if err := p.InitPluginRole(ctx, plugin); err != nil {
                         .....
                      }
                  }
                  err := p.registry.Add(ctx, plugin); 
              }
              return nil
          }

          從下面代碼中可以看到,加載plugin的過(guò)程就是:先從指定目錄下遍歷查找dist目錄下的plugin.json文件并讀取plugin.json中的內(nèi)容,保存在foundPlugins變量中, 然后為找到的所有plugin創(chuàng)建的對(duì)應(yīng)的plugin對(duì)象

                
                pluginJSONPaths, err := l.pluginFinder.Find(paths)
          for _, pluginJSONPath := range pluginJSONPaths {
                  plugin, err := l.readPluginJSON(pluginJSONPath)
                  pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
                  foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
              }

          loadedPlugins := make(map[string]*types.Plugin)
              for pluginDir, pluginJSON := range foundPlugins {
                  plugin := createPluginBase(pluginJSON, classpluginDir)
                  loadedPlugins[plugin.PluginDir
          = plugin
              }

          五、Plugin的渲染過(guò)程

          VelaUX中Plugin機(jī)制定義了路由規(guī)則,所有的Plugin頁(yè)面的路由地址都是"/plugins/:pluginId",pluginId是Plugin的id,而且都通過(guò)AppRootPage這個(gè)組件來(lái)渲染,如下代碼:

                
                 <Route
                  path="/plugins/:pluginId"
                  render={(props: any) => {
                    return <AppRootPage pluginId={props.match.params.pluginId}></AppRootPage>;
                  }}
                />

          AppRootPage組件中會(huì)去加載相應(yīng)的plugin并賦值給常量app, 而app.root就是Plugin中用戶(hù)開(kāi)發(fā)的需要在頁(yè)面上渲染的內(nèi)容,也就是我們?cè)陂_(kāi)發(fā)plugin時(shí)定在components目錄下定義的頁(yè)面App組件。

                
                function RootPage({ pluginId }: Props{
            const [app, setApp] = React.useState<AppPagePlugin>();
            React.useEffect(() => {
              loadAppPlugin(pluginId, setApp);
            }, [pluginId]);
            
            const AppRootPage = app.root
            return (<AppRootPage meta={app.meta} />);
          }

          怎么可以確認(rèn)app.root真的是我們新定義的App組件呢?我們可以查看我們定義插件時(shí)的mudule.ts文件,其中new了一個(gè)AppPagePlugin對(duì)象,并調(diào)用了setRootPage方法并將App作為參數(shù),而此App就是我們?cè)赾omponents中定義的App組件

                
                import { App } from './components/App';
          export const plugin = new AppPagePlugin<{}>().setRootPage(App).addConfigPage({
           ...
          });

          查看AppPagePlugin類(lèi)型的定可以看到其方法setRootPage就是將接收到到參數(shù)賦值給root屬性。

                
                export class AppPagePlugin {
            setRootPage(root) {
              this.root = root;
              return this;
            }
          }

          至此我們的Plugin頁(yè)面已經(jīng)渲染出來(lái)的。這里還有一個(gè)疑問(wèn)就是RootPage是如何將Plugin資源加載進(jìn)來(lái)的?這里是使用了SystemJS模塊加載器,通過(guò)SystemJS.import(path)加載進(jìn)來(lái)的模塊內(nèi)容,就是Plugin定義中的module.ts中導(dǎo)出的內(nèi)容,即:AppPagePlugin類(lèi)型的對(duì)象。

                async function importPluginModule(path: string, version?: string): Promise<any{
            return SystemJS.import(path);
          }
          function importAppPagePlugin(meta: PluginMeta): Promise<AppPagePlugin{
            return importPluginModule(meta.module, meta.info?.version).then((pluginExports) => {
              const plugin = pluginExports.plugin as AppPagePlugin;
              plugin.init(meta);
              plugin.meta = meta;
              return plugin;
            });
          }

          至于Plugin中用到的其他依賴(lài),則是通過(guò)SystemJS.registerDynamic提前將這些依賴(lài)注冊(cè)進(jìn)來(lái)的。

                export function exposeToPlugin(name: string, component: any{
            SystemJS.registerDynamic(name, [], true, (require: any, exports: any, module: { exports: any }) => {
              module.exports = component;
            });
          }
          exposeToPlugin('lodash', _);
          exposeToPlugin('moment', moment);
          exposeToPlugin('@velaux/data', velauxData);
          exposeToPlugin('@velaux/ui', velauxUI);
          exposeToPlugin('react', react);
          exposeToPlugin('react-dom', ReactDom);
          exposeToPlugin('redux', Redux);
          exposeToPlugin('dva/router', DvaRouter);

          至此Plugin頁(yè)面的渲染已經(jīng)全部完成,至于菜單的渲染就容易了,只需要在渲染菜單之前獲取一下Plugin列表,并根據(jù)相關(guān)菜單配置生成菜單項(xiàng)并渲染即可

                loadPluginMenus = () => {
              if (this.pluginLoaded) {
                return Promise.resolve(this.menus);
              }
              return getPluginSrv()
                .listAppPagePlugins()
                .then((plugins) => {
                  plugins.map((plugin) => {
                    plugin.includes?.map((include) => {
                      if (!this.menus.find((m) => m.name == include.name)) {
                        const pluginMenu: Menu = {
                          workspace: include.workspace.name,
                          type: include.type,
                          name: include.name,
                          label: include.label,
                          to: include.to,
                          relatedRoute: include.relatedRoute,
                          permission: include.permission,
                          catalog: include.catalog,
                        };
                        this.menus.push(pluginMenu);
                      }
                    });
                  });
                  this.pluginLoaded = true;
                  return Promise.resolve(this.menus);
                });
            };

          小結(jié)

          Plugin機(jī)制是VelaUX提供的一種擴(kuò)展機(jī)制,本文介紹了如何開(kāi)發(fā)一個(gè)Plugin,并通過(guò)對(duì)接口代理轉(zhuǎn)發(fā)、Plugin的加載、渲染等過(guò)程的代碼分析介紹了Plugin的核心實(shí)現(xiàn)原理。

          瀏覽 55
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  国产主播在线观看 | 免费成人毛片网站 | 一区二区三区在线 | 国产9在线观看黄A片免费 | 久操福利|