github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cluster/cluster_chart.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cluster
    21  
    22  import (
    23  	"compress/gzip"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  	"golang.org/x/exp/maps"
    29  	"golang.org/x/exp/slices"
    30  	"helm.sh/helm/v3/pkg/action"
    31  	"helm.sh/helm/v3/pkg/chart"
    32  	"helm.sh/helm/v3/pkg/chart/loader"
    33  	"helm.sh/helm/v3/pkg/chartutil"
    34  	"helm.sh/helm/v3/pkg/releaseutil"
    35  	"k8s.io/apimachinery/pkg/util/json"
    36  	"k8s.io/kube-openapi/pkg/validation/spec"
    37  	"k8s.io/kube-openapi/pkg/validation/strfmt"
    38  	"k8s.io/kube-openapi/pkg/validation/validate"
    39  
    40  	"github.com/1aal/kubeblocks/pkg/cli/util/helm"
    41  )
    42  
    43  const (
    44  	templatesDir = "templates"
    45  	clusterFile  = "cluster.yaml"
    46  )
    47  
    48  type SchemaPropName string
    49  
    50  // the common schema property name
    51  const (
    52  	VersionSchemaProp SchemaPropName = "version"
    53  	RBACEnabledProp   SchemaPropName = "rbacEnabled"
    54  )
    55  
    56  type ChartInfo struct {
    57  	// Schema is the cluster parent helm chart schema, used to render the command flag
    58  	Schema *spec.Schema
    59  
    60  	// SubSchema is the sub chart schema, used to render the command flag
    61  	SubSchema *spec.Schema
    62  
    63  	// SubChartName is the name (alias if exists) of the sub chart
    64  	SubChartName string
    65  
    66  	// ClusterDef is the cluster definition
    67  	ClusterDef string
    68  
    69  	// Chart is the cluster helm chart object
    70  	Chart *chart.Chart
    71  
    72  	// Alias is the alias of the cluster chart, will be used as the command alias
    73  	Alias string
    74  }
    75  
    76  func BuildChartInfo(t ClusterType) (*ChartInfo, error) {
    77  	var err error
    78  
    79  	c := &ChartInfo{}
    80  	// load helm chart from embed tgz file
    81  	if err = loadHelmChart(c, t); err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	if err = c.buildClusterSchema(); err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	return c, nil
    90  }
    91  
    92  // GetManifests gets the cluster manifests
    93  func GetManifests(c *chart.Chart, namespace, name, kubeVersion string, values map[string]interface{}) (map[string]string, error) {
    94  	// get the helm chart manifest
    95  	actionCfg, err := helm.NewActionConfig(helm.NewConfig(namespace, "", "", false))
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	actionCfg.Log = func(format string, v ...interface{}) {
   100  		fmt.Printf(format, v...)
   101  	}
   102  
   103  	// Parse Kubernetes version to fit the helm action config.
   104  	//
   105  	// We must set a valid Kubernetes version to render the manifests, otherwise
   106  	// helm will use a fake one that will cause the .Capabilities.KubeVersion.GitVersion
   107  	// return the fake version that is not expected.
   108  	v, err := chartutil.ParseKubeVersion(kubeVersion)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	client := action.NewInstall(actionCfg)
   114  	client.DryRun = true
   115  	client.Replace = true
   116  	client.ClientOnly = true
   117  	client.ReleaseName = name
   118  	client.Namespace = namespace
   119  	client.KubeVersion = v
   120  
   121  	rel, err := client.Run(c, values)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	return releaseutil.SplitManifests(rel.Manifest), nil
   126  }
   127  
   128  // buildClusterSchema build the schema for the given cluster chart.
   129  func (c *ChartInfo) buildClusterSchema() error {
   130  	var err error
   131  	cht := c.Chart
   132  	buildSchema := func(bs []byte) (*spec.Schema, error) {
   133  		schema := &spec.Schema{}
   134  		if err = json.Unmarshal(bs, schema); err != nil {
   135  			return nil, errors.Wrapf(err, "failed to build schema for engine %s", cht.Name())
   136  		}
   137  		return schema, nil
   138  	}
   139  
   140  	// build cluster schema
   141  	if c.Schema, err = buildSchema(cht.Schema); err != nil {
   142  		return err
   143  	}
   144  
   145  	if len(cht.Dependencies()) == 0 {
   146  		return nil
   147  	}
   148  
   149  	// build extra schema in sub chart, now, we only support one sub chart
   150  	subChart := cht.Dependencies()[0]
   151  	c.SubChartName = subChart.Name()
   152  	if c.SubSchema, err = buildSchema(subChart.Schema); err != nil {
   153  		return err
   154  	}
   155  
   156  	// if sub chart has alias, we should use alias instead of chart name
   157  	for _, dep := range cht.Metadata.Dependencies {
   158  		if dep.Name != c.SubChartName {
   159  			continue
   160  		}
   161  
   162  		if dep.Alias != "" {
   163  			c.SubChartName = dep.Alias
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (c *ChartInfo) buildClusterDef() error {
   171  	cht := c.Chart
   172  	// We use embed FS to read chart's tgz files. In embed FS, the file path format is compatible with Linux and does not change with the operating system.
   173  	// Therefore, we cannot use filepath.Join to generate different path formats for different systems,
   174  	// instead, we need to use a path format that is the same as Linux.
   175  	clusterFilePath := templatesDir + "/" + clusterFile
   176  	for _, tpl := range cht.Templates {
   177  		if tpl.Name != clusterFilePath {
   178  			continue
   179  		}
   180  
   181  		// get cluster definition from cluster.yaml
   182  		pattern := "  clusterDefinitionRef: "
   183  		str := string(tpl.Data)
   184  		start := strings.Index(str, pattern)
   185  		if start != -1 {
   186  			end := strings.IndexAny(str[start+len(pattern):], " \n")
   187  			if end != -1 {
   188  				c.ClusterDef = strings.TrimSpace(str[start+len(pattern) : start+len(pattern)+end])
   189  				return nil
   190  			}
   191  		}
   192  	}
   193  	return fmt.Errorf("failed to find the cluster definition of %s", cht.Name())
   194  }
   195  
   196  // ValidateValues validates the given values against the schema.
   197  func ValidateValues(c *ChartInfo, values map[string]interface{}) error {
   198  	validateFn := func(s *spec.Schema, values map[string]interface{}) error {
   199  		if s == nil {
   200  			return nil
   201  		}
   202  		v := validate.NewSchemaValidator(s, nil, "", strfmt.Default)
   203  		err := v.Validate(values).AsError()
   204  		if err != nil {
   205  			// the default error message is like "cpu in body should be a multiple of 0.5"
   206  			// the "in body" is not necessary, so we remove it
   207  			errMsg := strings.ReplaceAll(err.Error(), " in body", "")
   208  			return errors.New(errMsg)
   209  		}
   210  		return nil
   211  	}
   212  
   213  	if err := validateFn(c.Schema, values); err != nil {
   214  		return err
   215  	}
   216  	return validateFn(c.SubSchema, values)
   217  }
   218  
   219  func loadHelmChart(ci *ChartInfo, t ClusterType) error {
   220  	// cf references cluster config
   221  	cf, ok := ClusterTypeCharts[t]
   222  	if !ok {
   223  		return fmt.Errorf("failed to find the helm chart of %s", t)
   224  	}
   225  	file, err := cf.loadChart()
   226  	if err != nil {
   227  		return err
   228  	}
   229  	defer file.Close()
   230  
   231  	c, err := loader.LoadArchive(file)
   232  	if err != nil {
   233  		if err == gzip.ErrHeader {
   234  			return fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", cf.getChartFileName(), err)
   235  		}
   236  	}
   237  
   238  	if c == nil {
   239  		return fmt.Errorf("failed to load cluster helm chart %s", t)
   240  	}
   241  
   242  	ci.Chart = c
   243  	ci.Alias = cf.getAlias()
   244  	return nil
   245  }
   246  
   247  func SupportedTypes() []ClusterType {
   248  	types := maps.Keys(ClusterTypeCharts)
   249  	slices.SortFunc(types, func(i, j ClusterType) bool {
   250  		return i.String() < j.String()
   251  	})
   252  	return types
   253  }
   254  
   255  func (s SchemaPropName) String() string {
   256  	return string(s)
   257  }