程序员

源码解读:KubeVela 是如何将 appfile 转换为 K8s 特定资源对象

作者:admin 2021-04-05 我要评论

作者 | 樊大勇 KubeVela 是一个简单易用又高度可扩展的云原生应用管理引擎,是基于 Kubernetes 及阿里云与微软云共同发布的云原生应用开发模型 OAM 构建。 KubeV...

1.png

作者 | 樊大勇

KubeVela 是一个简单易用又高度可扩展的云原生应用管理引擎,是基于 Kubernetes 及阿里云与微软云共同发布的云原生应用开发模型 OAM 构建。

KubeVela 基于 OAM 模型构建了一套具体的实现,通过 Golang 编写,可以端到端地为用户构建云原生应用的平台,提供一个相对完整的解决方案

KubeVela 项目自 2020 年 7 月份在社区里面发起,受到包括阿里、微软、Crossplane 等公司工程师在内的广大社区志愿者的欢迎,并一起投入到项目开发工作中。他们把在 OAM 实践里面的各种经验与教训,都总结沉淀到 KubeVela 项目中。

本文主要目的是探索 KubeVela?如何将一个?appfile?文件转换为 K8s 中特定的资源对象。

该过程总的来说分为两个阶段:

appfile?转为 K8s 中的?applicationapplication?转换为对应的 K8s 资源对象
# vela.yaml
name: test
services:
 nginx:
 type: webservice
 image: nginx
 env:
 - name: NAME
 value: kubevela
 # svc trait
 svc:
 type: NodePort
 ports:
 - port: 80
 nodePort: 32017

利用 vela up 命令可以完成部署。

vela up 命令

建议:在看 vela 命令行工具代码之前,先去简单了解一下 cobra 框架。

// references/cli/up.go
// NewUpCommand will create command for applying an AppFile
func NewUpCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
 cmd := cobra.Command{
 Use: "up",
 DisableFlagsInUseLine: true,
 Short: "Apply an appfile",
 Long: "Apply an appfile",
 Annotations: map[string]string{
 types.TagCommandType: types.TypeStart,
 PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 return c.SetConfig()
 RunE: func(cmd *cobra.Command, args []string) error {
 velaEnv, err := GetEnv(cmd)
 if err != nil {
 return err
 kubecli, err := c.GetClient()
 if err != nil {
 return err
 o := common.AppfileOptions{
 Kubecli: kubecli,
 IO: ioStream,
 Env: velaEnv,
 filePath, err := cmd.Flags().GetString(appFilePath)
 if err != nil {
 return err
 return o.Run(filePath, velaEnv.Namespace, c)
 cmd.SetOut(ioStream.Out)
 cmd.Flags().StringP(appFilePath, "f", "", "specify file path for appfile")
 return cmd
}

上面源码展示的是 vela up 命令的入口。

在 PresistentPreRunE 函数中,通过调用 c.SetConfig() 完成 Kuberentes 配置信息 kubeconfig 的注入。

在 RunE 函数中:

首先,获取 vela 的 env 变量,velaEnv.Namespace 对应 Kubernetes 的命名空间。其次,获取 Kubernetes 的客户端,kubectl。接着,利用 Kubernetes 客户端和 vleaEnv 来构建渲染 Appfile 需要的 AppfileOptions。

最后,调用 o.Run(filePath, velaEnv.Namespace, c)。

该函数需要三个参数,其中 filePath 用于指定 appfile 的位置,velaEnv.Namespace 和 c 用来将渲染后的 Application 创建到指定命名空间。

filePath: appfile 的路径velaEnv.Namespace:对应 K8s 的 namespacec:K8s 客户端如何将一个 appfile 转为 Kubernetes 中的 Application起点:appfile终点:applicatioin

路径:appfile - application (services - component)

comp[workload, traits]1. 起点:AppFile
// references/appfile/api/appfile.go
// AppFile defines the spec of KubeVela Appfile
type AppFile struct {
 Name string `json:"name"`
 CreateTime time.Time `json:"createTime,omitempty"`
 UpdateTime time.Time `json:"updateTime,omitempty"`
 Services map[string]Service `json:"services"`
 Secrets map[string]string `json:"secrets,omitempty"`
 configGetter config.Store
 initialized bool
// NewAppFile init an empty AppFile struct
func NewAppFile() *AppFile {
 return AppFile{
 Services: make(map[string]Service),
 Secrets: make(map[string]string),
 configGetter: config.Local{},
}
// references/appfile/api/service.go
// Service defines the service spec for AppFile, it will contain all related information including OAM component, traits, source to image, etc...
type Service map[string]interface{}

上面两段代码是 AppFile 在客户端的声明,vela 会将指定路径的 yaml 文件读取后,赋值给一个 AppFile。

// references/appfile/api/appfile.go
// LoadFromFile will read the file and load the AppFile struct
func LoadFromFile(filename string) (*AppFile, error) {
 b, err := ioutil.ReadFile(filepath.Clean(filename))
 if err != nil {
 return nil, err
 af := NewAppFile()
 // Add JSON format appfile support
 ext := filepath.Ext(filename)
 switch ext {
 case ".yaml", ".yml":
 err = yaml.Unmarshal(b, af)
 case ".json":
 af, err = JSONToYaml(b, af)
 default:
 if json.Valid(b) {
 af, err = JSONToYaml(b, af)
 } else {
 err = yaml.Unmarshal(b, af)
 if err != nil {
 return nil, err
 return af, nil
}

下面为读取 vela.yaml 文件后,加载到 AppFile 中的数据:

# vela.yaml
name: test
services:
 nginx:
 type: webservice
 image: nginx
 env:
 - name: NAME
 value: kubevela
 # svc trait
 svc:
 type: NodePort
 ports:
 - port: 80
 nodePort: 32017
Name: test
CreateTime: 0001-01-01 00:00:00 +0000 UTC
UpdateTime: 0001-01-01 00:00:00 +0000 UTC
Services: map[
 nginx: map[
 env: [map[name: NAME value: kubevela]] 
 image: nginx 
 svc: map[ports: [map[nodePort: 32017 port: 80]] type: NodePort] 
 type: webservice
Secrets map[]
configGetter: 0x447abd0 
initialized: false
2. 终点:application
// apis/core.oam.dev/application_types.go
type Application struct {
 metav1.TypeMeta `json:",inline"`
 metav1.ObjectMeta `json:"metadata,omitempty"`
 Spec ApplicationSpec `json:"spec,omitempty"`
 Status AppStatus `json:"status,omitempty"`
// ApplicationSpec is the spec of Application
type ApplicationSpec struct {
 Components []ApplicationComponent `json:"components"`
 // TODO(wonderflow): we should have application level scopes supported here
 // RolloutPlan is the details on how to rollout the resources
 // The controller simply replace the old resources with the new one if there is no rollout plan involved
 // +optional
 RolloutPlan *v1alpha1.RolloutPlan `json:"rolloutPlan,omitempty"`
}

上面代码,为 Application 的声明,结合 .vela/deploy.yaml(见下面代码),可以看出,要将一个 AppFile 渲染为 Application 主要就是将 AppFile 的 Services 转化为 Application 的 Components。

# .vela/deploy.yaml
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
 creationTimestamp: null
 name: test
 namespace: default
spec:
 components:
 - name: nginx
 scopes:
 healthscopes.core.oam.dev: test-default-health
 settings:
 env:
 - name: NAME
 value: kubevela
 image: nginx
 traits:
 - name: svc
 properties:
 ports:
 - nodePort: 32017
 port: 80
 type: NodePort
 type: webservice
status: {}
3. 路径:Services - Components

结合以上内容可以看出,将 Appfile 转化为 Application 主要是将 Services 渲染为 Components。

// references/appfile/api/appfile.go
// BuildOAMApplication renders Appfile into Application, Scopes and other K8s Resources.
func (app *AppFile) BuildOAMApplication(env *types.EnvMeta, io cmdutil.IOStreams, tm template.Manager, silence bool) (*v1alpha2.Application, []oam.Object, error) {
 servApp := new(v1alpha2.Application)
 servApp.SetNamespace(env.Namespace)
 servApp.SetName(app.Name)
 servApp.Spec.Components = []v1alpha2.ApplicationComponent{}
 for serviceName, svc := range app.GetServices() {
 // 完成 Service 到 Component 的转化
 comp, err := svc.RenderServiceToApplicationComponent(tm, serviceName)
 if err != nil {
 return nil, nil, err
 servApp.Spec.Components = append(servApp.Spec.Components, comp)
 servApp.SetGroupVersionKind(v1alpha2.SchemeGroupVersion.WithKind("Application"))
 auxiliaryObjects = append(auxiliaryObjects, addDefaultHealthScopeToApplication(servApp))
 return servApp, auxiliaryObjects, nil
}

上面的代码是 vela 将 Appfile 转化为 Application 代码实现的位置。其中 comp, err := svc.RenderServiceToApplicationComponent(tm, serviceName) 完成 Service 到 Component 的转化。

// references/appfile/api/service.go
// RenderServiceToApplicationComponent render all capabilities of a service to CUE values to KubeVela Application.
func (s Service) RenderServiceToApplicationComponent(tm template.Manager, serviceName string) (v1alpha2.ApplicationComponent, error) {
 // sort out configs by workload/trait
 workloadKeys := map[string]interface{}{}
 var traits []v1alpha2.ApplicationTrait
 wtype := s.GetType()
 comp := v1alpha2.ApplicationComponent{
 Name: serviceName,
 WorkloadType: wtype,
 for k, v := range s.GetApplicationConfig() {
 // 判断是否为 trait
 if tm.IsTrait(k) {
 trait := v1alpha2.ApplicationTrait{
 Name: k,
 ....
 // 如果是 triat 加入 traits 中
 traits = append(traits, trait)
 continue
 workloadKeys[k] = v
 // Handle workloadKeys to settings
 settings := runtime.RawExte nsion{}
 pt, err := json.Marshal(workloadKeys)
 if err != nil {
 return comp, err
 if err := settings.UnmarshalJSON(pt); err != nil {
 return comp, err
 comp.Settings = *settings
 if len(traits) 0 {
 comp.Traits = traits
 return comp, nil
}
4. 总结

执行 vela up 命令,渲染 appfile 为 Application,将数据写入到 .vela/deploy.yaml 中,并在 K8s 中创建。

2.png

Application 是如何转换为对应 K8s 资源对象起点:Application中点:ApplicationConfiguration, Component终点:Deployment, Service

路径:

application_controllerapplicationconfiguration controller

【建议】 了解一下内容: - client-to

controller-runtimeoperator1.?Application
# 获取集群中的 Application
$ kubectl get application
NAMESPACE NAME AGE
default test 24h
2. ApplicationConfiguration 和 Component

当 application controller 获取到 Application 资源对象之后,会根据其内容创建出对应的 ApplicationConfiguration 和 Component。

# 获取 ApplicationConfiguration 和 Component
$ kubectl get ApplicationConfiguration,Component
NAME AGE
applicationconfiguration.core.oam.dev/test 24h
NAME WORKLOAD-KIND AGE
component.core.oam.dev/nginx Deployment 24h

ApplicationiConfiguration 中以名字的方式引入 Component:

3.png

3. application controller基本逻辑:获取一个 Application 资源对象。将 Application 资源对象渲染为 ApplicationConfiguration 和 Component。创建 ApplicationConfiguration 和 Component 资源对象。代码:
// pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go
// Reconcile process app event
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 ctx := context.Background()
 applog := r.Log.WithValues("application", req.NamespacedName)
 // 1. 获取 Application
 app := new(v1alpha2.Application)
 if err := r.Get(ctx, client.ObjectKey{
 Name: req.Name,
 Namespace: req.Namespace,
 }, app); err != nil {
 // 2. 将 Application 转换为 ApplicationConfiguration 和 Component
 handler := appHandler{r, app, applog}
 appParser := appfile.NewApplicationParser(r.Client, r.dm)
 appfile, err := appParser.GenerateAppFile(ctx, app.Name, app)
 ac, comps, err := appParser.GenerateApplicationConfiguration(appfile, app.Namespace)
 // 3. 在集群中创建 ApplicationConfiguration 和 Component 
 // apply appConfig component to the cluster
 if err := handler.apply(ctx, ac, comps); err != nil {
 applog.Error(err, "[Handle apply]")
 app.Status.SetConditions(errorCondition("Applied", err))
 return handler.handleErr(err)
 return ctrl.Result{}, r.UpdateStatus(ctx, app)
}
4. applicationconfiguration controller基本逻辑:获取 ApplicationConfiguration 资源对象。循环遍历,获取每一个 Component 并将 workload 和 trait 渲染为对应的 K8s 资源对象。创建对应的 K8s 资源对象。代码:
// pkg/controller/core.oam.dev/v1alpha2/applicationcinfiguratioin/applicationconfiguratioin.go
// Reconcile an OAM ApplicationConfigurations by rendering and instantiating its
// Components and Traits.
func (r *OAMApplicationReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
 ac := v1alpha2.ApplicationConfiguration{}
 // 1. 获取 ApplicationConfiguration
 if err := r.client.Get(ctx, req.NamespacedName, ac); err != nil {
 return r.ACReconcile(ctx, ac, log)
// ACReconcile contains all the reconcile logic of an AC, it can be used by other controller
func (r *OAMApplicationReconciler) ACReconcile(ctx context.Context, ac *v1alpha2.ApplicationConfiguration,
 log logging.Logger) (result reconcile.Result, returnErr error) {
 // 2. 渲染
 // 此处 workloads 包含所有Component对应的的 workload 和 tratis 的 k8s 资源对象
 workloads, depStatus, err := r.components.Render(ctx, ac)
 applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode, log)}
 // 3. 创建 workload 和 traits 对应的 k8s 资源对象
 if err := r.workloads.Apply(ctx, ac.Status.Workloads, workloads, applyOpts...); err != nil {
 // the defer function will do the final status update
 return reconcile.Result{RequeueAfter: waitTime}, nil
}
5. 总结

当 vela up 将一个 AppFile 渲染为一个 Application 后,后续的流程由 application controller 和 applicationconfiguration controller 完成。

4.png

作者简介

樊大勇,华胜天成研发工程师,GitHub ID:@just-do1。

加入 OAMOAM 官网:

https://oam.dev

KubeVela GitHub 项目地址:

https://github.com/oam-dev/kubevela

社区交流钉群:

5.png


本文转自网络,原文链接:https://developer.aliyun.com/article/783169

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • 我为什么最终放弃了 Linux 桌面版的研

    我为什么最终放弃了 Linux 桌面版的研

  • 这是什么“虎狼之词”啊!!!程序员的

    这是什么“虎狼之词”啊!!!程序员的

  • 我丢,GitHub 上竟然还有这种骚操作,

    我丢,GitHub 上竟然还有这种骚操作,

  • C++ 运算符重载中返回值的坑

    C++ 运算符重载中返回值的坑

腾讯云代理商
海外云服务器