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 }