动机

Helm作为Kubernetes的包管理工具,极大的方便了Kubernetes应用程序的管控。
然而,Helm却仅仅提供了命令的方式对Kubernetes集群的应用程序进行管控,当我们要基于Kubernetes构建一个PaaS或SaaS容器云的时候,通常会有一个客户端微服务或程序需要调用Helm来进行应用程序的管控。在这种情况下通过直接调用Helm命令的方式进行应用管控终究不是很好的方式。
从我个人的调研情况来看,Helm官方或开源社区并没有Helm的RPC或是RESTful接口。至少本人没有找到,各位大牛如果有谁知道的话,还请不吝告知。
本文将通过封装Helm的install命令为例子来讲解如何将Helm的命令封装成RESTful API,以便提供给客户端程序调用。

代码示例展示

本文采用gin web框架进行RESTful API的封装。
直接贴代码,如有问题请留言指正,谢谢。
下边为main程序:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"helm-proxy/route"
	"helm-proxy/utils"
)

func main() {
	gin.SetMode(gin.DebugMode)
	router := route.SetupRouter()

	// listen and serve on 0.0.0.0:8080
	if err := router.Run(":8080"); err != nil {
		fmt.Printf("Service: %s is exiting\n", "helm-proxy")
		fmt.Printf("Error info: %v\n", err)
	}
}

下述代码为gin的router的配置:

package route

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"helm-proxy/controllers"
	"time"
)

var Router *gin.Engine
func init() {
	Router = gin.New()
	Router.Use(gin.LoggerWithFormatter(loggerFormat))
	Router.Use(gin.Recovery())
}

func loggerFormat(param gin.LogFormatterParams) string {
	// your custom format
	return fmt.Sprintf("[%s] \"%s %s %s %d %s \"%s\" %s\"\n",
		param.TimeStamp.Format(time.RFC1123),
		param.Method,
		param.Path,
		param.Request.Proto,
		param.StatusCode,
		param.Latency,
		param.Request.UserAgent(),
		param.ErrorMessage)
}

func SetupRouter() *gin.Engine {
	appName := “helm-proxy”
	appsV1 := Router.Group(appName + "/v1/namespaces")
	{
	    //本文只以Install命令为例
		appsV1.POST("/:nsName/releases", controllers.Install)
		appsV1.DELETE("/:nsName/releases/:relName", controllers.Uninstall)
		appsV1.GET("/:nsName/releases/:relName/status", controllers.Status)
	}
	return Router
}

下边代码为gin的router的Handler程序:

package controllers

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"helm-proxy/constant"
	"helm-proxy/services"
	"net/http"
)

func Install(context *gin.Context) {
	respBody := make(map[string]interface{})

	nsName := context.Param("nsName")
	// Get request body
	var reqBody constant.InstallReqBody
	err := context.BindJSON(&reqBody)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		context.JSON(http.StatusBadRequest, respBody)
		return
	}

	code, err := services.Install(nsName, reqBody)
	respBody = makeRespBody(code, err)
	respBody["data"] = reqBody

	context.JSON(http.StatusOK, respBody)
}

下属代码为helm命令中主要的配置实现:

package actionconfig

import (
	"context"
	"fmt"
	auth "github.com/deislabs/oras/pkg/auth/docker"
	"helm.sh/helm/pkg/action"
	"helm.sh/helm/pkg/cli"
	"helm.sh/helm/pkg/helmpath"
	"helm.sh/helm/pkg/kube"
	"helm.sh/helm/pkg/registry"
	"helm.sh/helm/pkg/storage"
	"helm.sh/helm/pkg/storage/driver"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/util/homedir"
	"log"
	"os"
	"path/filepath"
	"sync"
)

var (
	settings cli.EnvSettings
)

func GetActionConfig(ns string) *action.Configuration {
	actionConfig := new(action.Configuration)
	initActionConfig(actionConfig, false, ns)
	return actionConfig
}

func GetSettings() cli.EnvSettings {
	return settings
}

func initActionConfig(actionConfig *action.Configuration, allNamespaces bool, ns string) {
	settings.Home = (helmpath.Home)(filepath.Join(homedir.HomeDir(), ".helm"))
	settings.Namespace = ns

	credentialsFile := filepath.Join(settings.Home.Registry(), registry.CredentialsFileBasename)
	client, err := auth.NewClient(credentialsFile)
	if err != nil {
		panic(err)
	}
	resolver, err := client.Resolver(context.Background())
	if err != nil {
		panic(err)
	}

	actionConfig.RegistryClient = registry.NewClient(&registry.ClientOptions{
		Debug: settings.Debug,
		Out:   os.Stdout,
		Authorizer: registry.Authorizer{
			Client: client,
		},
		Resolver: registry.Resolver{
			Resolver: resolver,
		},
		CacheRootDir: settings.Home.Registry(),
	})

	kc := kube.New(kubeConfig())
	kc.Log = logf

	clientset, err := kc.KubernetesClientSet()
	if err != nil {
		// TODO return error
		log.Fatal(err)
	}
	var namespace string
	if !allNamespaces {
		namespace = GetNamespace()
	}

	var store *storage.Storage
	switch os.Getenv("HELM_DRIVER") {
	case "secret", "secrets", "":
		d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
		d.Log = logf
		store = storage.Init(d)
	case "configmap", "configmaps":
		d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
		d.Log = logf
		store = storage.Init(d)
	case "memory":
		d := driver.NewMemory()
		store = storage.Init(d)
	default:
		// Not sure what to do here.
		panic("Unknown driver in HELM_DRIVER: " + os.Getenv("HELM_DRIVER"))
	}

	actionConfig.RESTClientGetter = kubeConfig()
	actionConfig.KubeClient = kc
	actionConfig.Releases = store
	actionConfig.Log = logf
}

func kubeConfig() genericclioptions.RESTClientGetter {
	var configOnce sync.Once
	var config genericclioptions.RESTClientGetter
	configOnce.Do(func() {
		config = kube.GetConfig(settings.KubeConfig, settings.KubeContext, settings.Namespace)
	})
	return config
}

func GetNamespace() string {
	if ns, _, err := kubeConfig().ToRawKubeConfigLoader().Namespace(); err == nil {
		fmt.Printf("Settings.Namespace = %s, ns = %s \n", settings.Namespace, ns)
		return ns
	}
	return "default"
}

func logf(format string, v ...interface{}) {
	if settings.Debug {
		format = fmt.Sprintf("[debug] %s\n", format)
		log.Output(2, fmt.Sprintf(format, v...))
	}
}

下述代码为Install命令的主要执行逻辑:

package services

import (
	"fmt"
	"github.com/pkg/errors"
	"helm-proxy/actionconfig"
	"helm-proxy/constant"
	"helm.sh/helm/pkg/action"
	"helm.sh/helm/pkg/chart"
	"helm.sh/helm/pkg/chart/loader"
	"helm.sh/helm/pkg/downloader"
	"helm.sh/helm/pkg/getter"
	"helm.sh/helm/pkg/release"
	"k8s.io/client-go/util/homedir"
	"os"
	"path/filepath"
	"time"
)

func Install(nsName string, reqBody constant.InstallReqBody) (string, error) {
	fmt.Println("Install starting....")
	actionConfig := actionconfig.GetActionConfig(nsName)

	client := action.NewInstall(actionConfig)
	configInstallFlag(client)
	configInstallValues(client, reqBody.Values)
	configChartPathOptionsFlags(client)

	rel, err := runInstall(client, reqBody)
	if err != nil {
		fmt.Println("err: ", err.Error())
		return constant.ErrorCode, err
	}

	action.PrintRelease(os.Stdout, rel)
	fmt.Println("Install ending....")

	if rel.Info.Status.String() == "deployed" {
		return constant.SuccessCode, nil
	} else {
		return constant.ErrorCode, fmt.Errorf(rel.Info.Status.String())
	}
}

func runInstall(client *action.Install, reqBody constant.InstallReqBody) (*release.Release, error) {
	fmt.Println("runInstall starting....")

	fmt.Printf("Original chart version: %q", client.Version)
	if client.Version == "" && client.Devel {
		fmt.Printf("setting version to >0.0.0-0")
		client.Version = ">0.0.0-0"
	}

	name := reqBody.AppName
	chart := reqBody.Repo + "/" + reqBody.ChartName
	client.ReleaseName = name

	cp, err := client.ChartPathOptions.LocateChart(chart, actionconfig.GetSettings())
	if err != nil {
		return nil, err
	}

	fmt.Printf("CHART PATH: %s\n", cp)

	if err := client.ValueOptions.MergeValues(actionconfig.GetSettings()); err != nil {
		return nil, err
	}

	// Check chart dependencies to make sure all are present in /charts
	chartRequested, err := loader.Load(cp)
	if err != nil {
		return nil, err
	}

	validInstallableChart, err := isChartInstallable(chartRequested)
	if !validInstallableChart {
		return nil, err
	}

	if req := chartRequested.Metadata.Dependencies; req != nil {
		// If CheckDependencies returns an error, we have unfulfilled dependencies.
		// As of Helm 2.4.0, this is treated as a stopping condition:
		// https://github.com/helm/helm/issues/2209
		if err := action.CheckDependencies(chartRequested, req); err != nil {
			if client.DependencyUpdate {
				man := &downloader.Manager{
					Out:        os.Stdout,
					ChartPath:  cp,
					HelmHome:   actionconfig.GetSettings().Home,
					Keyring:    client.ChartPathOptions.Keyring,
					SkipUpdate: false,
					Getters:    getter.All(actionconfig.GetSettings()),
				}
				if err := man.Update(); err != nil {
					return nil, err
				}
			} else {
				return nil, err
			}
		}
	}

	client.Namespace = actionconfig.GetNamespace()
	fmt.Println("runInstall ending....")
	return client.Run(chartRequested)
}

// 后续需要根据传进来的请求body进行相应的赋值,此处只是赋值为默认值
func configInstallFlag(client *action.Install) {
	client.DryRun = false
	client.DisableHooks = false
	client.Replace = false
	client.Timeout = 300 * time.Second
	client.Wait = false
	client.GenerateName = false
	client.NameTemplate = ""
	client.Devel = false
	client.DependencyUpdate = false
	client.Atomic = false
}

func configChartPathOptionsFlags(client *action.Install) {
	client.Version = ""
	client.Verify = false
	client.Keyring = defaultKeyring()
	client.RepoURL = ""
	client.Username = ""
	client.Password = ""
	client.CertFile = ""
	client.KeyFile = ""
	client.CaFile = ""
}

func configInstallValues(client *action.Install, values []constant.ValueBody) {
	for i := 0; i < len(values); i++ {
		value := values[i].Key + "=" + values[i].Value
		client.ValueOptions.Values = append(client.ValueOptions.Values, value)
	}
}

// defaultKeyring returns the expanded path to the default keyring.
func defaultKeyring() string {
	if v, ok := os.LookupEnv("GNUPGHOME"); ok {
		return filepath.Join(v, "pubring.gpg")
	}
	return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
}

// isChartInstallable validates if a chart can be installed
// Application chart type is only installable
func isChartInstallable(ch *chart.Chart) (bool, error) {
	switch ch.Metadata.Type {
	case "", "application":
		return true, nil
	}
	return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}

至此,我们完成了将Helm Install命令封装为Restful命令的所有工作。

结果验证

以下为通过postman发送http请求执行Helm Install的结果:
在这里插入图片描述在命令行通过helm命令的查询结果:
在这里插入图片描述

总结

从上述封装的过程来看,将Helm的命令进行封装以提供RESTful风格的API还是比较简单的,主要是讲helm中cmd目录下的代码进行处理,即可以完成大部分的封装命令。
后续我将继续完成对Helm剩余命令的封装。

本文仅为个人在项目工作过程中的实践总结,如果有错误或各位大牛有更好的方式,还请多多指教。

Logo

开源、云原生的融合云平台

更多推荐