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(®istry.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剩余命令的封装。
本文仅为个人在项目工作过程中的实践总结,如果有错误或各位大牛有更好的方式,还请多多指教。