github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cluster/external_charts.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 "io" 26 "io/fs" 27 "os" 28 "path/filepath" 29 30 "github.com/spf13/cobra" 31 "gopkg.in/yaml.v2" 32 "helm.sh/helm/v3/pkg/chart/loader" 33 "k8s.io/klog" 34 35 "github.com/1aal/kubeblocks/pkg/cli/types" 36 "github.com/1aal/kubeblocks/pkg/cli/util" 37 "github.com/1aal/kubeblocks/pkg/cli/util/flags" 38 ) 39 40 // CliClusterChartConfig is $HOME/.kbcli/cluster_types by default 41 var CliClusterChartConfig string 42 43 // CliChartsCacheDir is $HOME/.kbcli/charts by default 44 var CliChartsCacheDir string 45 46 type clusterConfig []*TypeInstance 47 48 // GlobalClusterChartConfig is kbcli global cluster chart config reference to CliClusterChartConfig 49 var GlobalClusterChartConfig clusterConfig 50 var CacheFiles []fs.DirEntry 51 52 // ReadConfigs read the config from configPath 53 func (c *clusterConfig) ReadConfigs(configPath string) error { 54 contents, err := os.ReadFile(configPath) 55 if err != nil { 56 if !os.IsNotExist(err) { 57 return err 58 } 59 contents = []byte{} 60 } 61 err = yaml.Unmarshal(contents, c) 62 if err != nil { 63 return err 64 } 65 return nil 66 } 67 68 // WriteConfigs write current config into configPath 69 func (c *clusterConfig) WriteConfigs(configPath string) error { 70 newConfig, err := yaml.Marshal(*c) 71 if err != nil { 72 return err 73 } 74 return os.WriteFile(configPath, newConfig, 0666) 75 } 76 77 // AddConfig add a new cluster type instance into current config 78 func (c *clusterConfig) AddConfig(add *TypeInstance) { 79 *c = append(*c, add) 80 } 81 82 // UpdateConfig will update the existed TypeInstance in c 83 func (c *clusterConfig) UpdateConfig(update *TypeInstance) { 84 for i, instance := range *c { 85 if instance.Name == update.Name { 86 (*c)[i] = update 87 } 88 } 89 } 90 91 // RemoveConfig remove a ClusterType from current config 92 func (c *clusterConfig) RemoveConfig(name ClusterType) bool { 93 tempList := *c 94 for i, chart := range tempList { 95 if chart.Name == name { 96 *c = append((*c)[:i], (*c)[i+1:]...) 97 return true 98 } 99 } 100 return false 101 } 102 func (c *clusterConfig) Len() int { 103 return len(*c) 104 } 105 106 // RegisterCMD will register all cluster type instances in the config c and auto clear the register failed instances 107 // and rewrite config 108 func RegisterCMD(c clusterConfig, configPath string) { 109 var needRemove []ClusterType 110 for _, config := range c { 111 if err := config.register(config.Name); err != nil { 112 klog.V(2).Info(err.Error()) 113 needRemove = append(needRemove, config.Name) 114 } 115 } 116 for _, name := range needRemove { 117 c.RemoveConfig(name) 118 } 119 if err := c.WriteConfigs(configPath); err != nil { 120 klog.V(2).Info(fmt.Sprintf("Warning: auto clear kbcli cluster chart config failed %s\n", err.Error())) 121 } 122 } 123 124 func GetChartCacheFiles() []fs.DirEntry { 125 homeDir, _ := util.GetCliHomeDir() 126 dirFS := os.DirFS(homeDir) 127 result, err := fs.ReadDir(dirFS, "charts") 128 if err != nil { 129 if os.IsNotExist(err) { 130 err = os.MkdirAll(CliChartsCacheDir, 0777) 131 if err != nil { 132 klog.V(2).Info(fmt.Sprintf("Failed to create charts cache dir %s: %s", CliChartsCacheDir, err.Error())) 133 return nil 134 } 135 result = []fs.DirEntry{} 136 } else { 137 klog.V(2).Info(fmt.Sprintf("Failed to create charts cache dir %s: %s", CliChartsCacheDir, err.Error())) 138 return nil 139 } 140 } 141 return result 142 } 143 144 func ClearCharts(c ClusterType) { 145 // if the fail clusterType is from external config, remove the config and the elated charts 146 if GlobalClusterChartConfig.RemoveConfig(c) { 147 if err := GlobalClusterChartConfig.WriteConfigs(CliClusterChartConfig); err != nil { 148 klog.V(2).Info(fmt.Sprintf("Warning: auto clear %s config fail due to: %s\n", c, err.Error())) 149 150 } 151 if err := os.Remove(filepath.Join(CliChartsCacheDir, ClusterTypeCharts[c].getChartFileName())); err != nil { 152 klog.V(2).Info(fmt.Sprintf("Warning: auto clear %s config fail due to: %s\n", c, err.Error())) 153 } 154 CacheFiles = GetChartCacheFiles() 155 } 156 } 157 158 // TypeInstance reference to a cluster type instance in config 159 type TypeInstance struct { 160 Name ClusterType `yaml:"name"` 161 URL string `yaml:"helmChartUrl"` 162 Alias string `yaml:"alias"` 163 // chartName is the filename cached locally 164 ChartName string `yaml:"chartName"` 165 } 166 167 // PreCheck is used by `cluster register` command 168 func (h *TypeInstance) PreCheck() error { 169 chartInfo := &ChartInfo{} 170 // load helm chart from embed tgz file 171 { 172 file, err := h.loadChart() 173 if err != nil { 174 return err 175 } 176 defer file.Close() 177 c, err := loader.LoadArchive(file) 178 if err != nil { 179 if err == gzip.ErrHeader { 180 return fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", h.getChartFileName(), err) 181 } 182 } 183 if c == nil { 184 return fmt.Errorf("failed to load cluster helm chart %s", h.getChartFileName()) 185 } 186 chartInfo.Chart = c 187 } 188 if err := chartInfo.buildClusterSchema(); err != nil { 189 return err 190 } 191 if err := chartInfo.buildClusterDef(); err != nil { 192 return err 193 } 194 195 // pre-check build sub-command flags 196 if err := flags.BuildFlagsBySchema(&cobra.Command{}, chartInfo.Schema); err != nil { 197 return err 198 } 199 200 return flags.BuildFlagsBySchema(&cobra.Command{}, chartInfo.SubSchema) 201 } 202 203 func (h *TypeInstance) loadChart() (io.ReadCloser, error) { 204 return os.Open(filepath.Join(CliChartsCacheDir, h.getChartFileName())) 205 } 206 207 func (h *TypeInstance) getChartFileName() string { 208 return h.ChartName 209 } 210 211 func (h *TypeInstance) getAlias() string { 212 return h.Alias 213 } 214 215 func (h *TypeInstance) register(subcmd ClusterType) error { 216 if _, ok := ClusterTypeCharts[subcmd]; ok { 217 return fmt.Errorf("cluster type %s already registered", subcmd) 218 } 219 ClusterTypeCharts[subcmd] = h 220 221 for _, f := range CacheFiles { 222 if f.Name() == h.getChartFileName() { 223 return nil 224 } 225 } 226 return fmt.Errorf("can't find the %s in cache, please use 'kbcli cluster pull %s --url %s' first", h.Name.String(), h.Name.String(), h.URL) 227 } 228 229 var _ chartLoader = &TypeInstance{} 230 231 func init() { 232 homeDir, _ := util.GetCliHomeDir() 233 CliClusterChartConfig = filepath.Join(homeDir, types.CliClusterTypeConfigs) 234 CliChartsCacheDir = filepath.Join(homeDir, types.CliChartsCache) 235 236 err := GlobalClusterChartConfig.ReadConfigs(CliClusterChartConfig) 237 if err != nil { 238 fmt.Println(err.Error()) 239 return 240 } 241 // check charts cache dir 242 CacheFiles = GetChartCacheFiles() 243 RegisterCMD(GlobalClusterChartConfig, CliClusterChartConfig) 244 }