The author | da-yong fan

KubeVela is an easy-to-use and highly scalable cloud native application management engine, which is built on the cloud native application development model OAM jointly published by Kubernetes, Ali Cloud and Microsoft Cloud.

KubeVela builds a set of concrete implementation based on OAM model, which is written by Golang to provide a relatively complete solution for users to build cloud native application platform end-to-end.

The KubeVela project was launched in The community in July 2020, and was welcomed by a large number of community volunteers, including engineers from Alibaba, Microsoft, Crossplane and other companies, who worked together to develop the project. They summarized and deposited all kinds of experience and lessons in OAM practice into KubeVela project.

The main purpose of this article is to explore how KubeVela converts an appFile file into a specific resource object in K8s.

The process is generally divided into two stages:

  1. Appfile becomes application in K8s
  2. Application is converted to the corresponding K8s resource object
# 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
Copy the code

The deployment can be accomplished using the Vela up command.

Vela up command

Tip: Before looking at the Vela command line tool code, take a quick look at the Cobra framework.

// 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 }Copy the code

The source code above shows the entry to the Vela up command.

In the PresistentPreRunE function, the injection of kubeconfig Kuberentes configuration information is done by calling c.setconfig ().

In the RunE function:

  • First, get Vela’s env variable. Velaenv. Namespace corresponds to Kubernetes’ Namespace.

  • Second, get Kubernetes client, Kubectl.

  • Next, use the Kubernetes client and vleaEnv to build the AppfileOptions required to render the Appfile.

  • Finally, call O.run (filePath, velaenv.namespace, c).

    • The function takes three arguments, filePath to specify the location of the appfile, velaenv.namespace, and c to create the rendered Application into the specified Namespace.
      • FilePath: indicates the path of appFile
      • Velaenv. Namespace: indicates the Namespace of K8s
      • C: K8s client

How to convert an Appfile to an Application in Kubernetes

  • Starting point: appfile

  • Destination: applicatioin

  • Appfile -> Application (Services -> Component)

    • comp[workload, traits]

1. Starting point: 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{}, } }Copy the code
// 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{}
Copy the code

The above two pieces of code are declarations of AppFile on the client side. Vela will read the yamL file in the specified path and assign the value to an 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 }Copy the code

The following is the data loaded into the AppFile after reading the vela.yaml file:

# 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
Copy the code
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: falseCopy the code

2. End: 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"` }Copy the code

The above code is the Application declaration, combined with.vela/deploy.yaml (see the code below). To render an AppFile as Application, convert the AppFile’s Services to 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: {}
Copy the code

3. Path: Services -> Components

From the above, we can see that converting Appfile to Application is mainly to render Services as 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() { ... / / complete Service to the transformation of the 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 }Copy the code

The code above is where Vela converts appFiles into Application code implementations. The comp, err: = SVC. RenderServiceToApplicationComponent (tm, serviceName) complete Service to the transformation of the 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.G etApplicationConfig () {/ / determine whether for trait. If tm IsTrait (k) {trait: = v1alpha2 ApplicationTrait {Name: k, } .... 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 }Copy the code

4. To summarize

Execute Vela up, render appfile as Application, write the data to.vela/deploy.yaml, and create it in K8s.

How is Application converted to the corresponding K8s resource object

  • Starting point: Application
  • Neutral: ApplicationConfiguration, Component
  • End point: Deployment, Service
  • Path:
    • application_controller
    • applicationconfiguration controller

[Suggestion] > Learn about the content: > -client-to

  • controller-runtime
  • operator

1. Application

$kubectl get Application NAMESPACE NAME AGE Default test 24hCopy the code

2. ApplicationConfiguration and Component

When the Application Controller obtains the Application resource object, it creates the corresponding ApplicationConfiguration and Component based on its contents.

# ApplicationConfiguration and Component $kubectl get ApplicationConfiguration, Component NAME AGE applicationconfiguration.core.oam.dev/test 24h NAME WORKLOAD-KIND AGE component.core.oam.dev/nginx Deployment 24hCopy the code

ApplicationiConfiguration in introducing Component in the form of name:

3. application controller

Basic logic:
  • Get an Application resource object.

  • Render the Application resource object as ApplicationConfiguration and Component.

  • Create ApplicationConfiguration and Component resource objects.

Code:
// 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.et (CTX, client.objectKey {Name: req.name, Namespace: req.Namespace, }, app); err ! = nil { ... }... // convert Application to ApplicationConfiguration and 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) ... // Create ApplicationConfiguration and Component in the cluster // 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) }Copy the code

4. applicationconfiguration controller

Basic logic:
  • Gets the ApplicationConfiguration resource object.

  • Loop through, fetching each Component and rendering the workload and traits as corresponding K8s resource objects.

  • Create the corresponding K8s resource object.

Code:
// 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) { ... Workloads, depStatus, workloads, loads, loads, loads, loads, loads err := r.components.Render(ctx, ac) ... applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode, log)} // 3. If err := r.vorkloads.Apply(CTX, ac.status. Workloads, Workloads, applyOpts...) ; err ! = nil { ... }... // the defer function will do the final status update return reconcile.Result{RequeueAfter: waitTime}, nil }Copy the code

5. To summarize

After Vela Up renders an AppFile as an Application, the subsequent flow is done by the Application Controller and ApplicationConfiguration Controller.

Author’s brief introduction

Fan Dayong, R&D Engineer of Huasheng Tiancheng, GitHub ID: @just-do1

Join the OAM

  • The OAM website:

oam.dev

  • KubeVela GitHub Project

Github.com/oam-dev/kub…