github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cloudprovider/k3d.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 cloudprovider 21 22 import ( 23 "context" 24 _ "embed" 25 "fmt" 26 "io" 27 "net" 28 "os" 29 "strconv" 30 "strings" 31 32 "github.com/docker/go-connections/nat" 33 "github.com/k3d-io/k3d/v5/pkg/actions" 34 k3dClient "github.com/k3d-io/k3d/v5/pkg/client" 35 config "github.com/k3d-io/k3d/v5/pkg/config/v1alpha5" 36 l "github.com/k3d-io/k3d/v5/pkg/logger" 37 "github.com/k3d-io/k3d/v5/pkg/runtimes" 38 k3d "github.com/k3d-io/k3d/v5/pkg/types" 39 "github.com/k3d-io/k3d/v5/pkg/types/fixes" 40 "github.com/pkg/errors" 41 "github.com/sirupsen/logrus" 42 "k8s.io/client-go/tools/clientcmd" 43 "k8s.io/klog/v2" 44 45 "github.com/1aal/kubeblocks/pkg/cli/types" 46 "github.com/1aal/kubeblocks/pkg/cli/util" 47 "github.com/1aal/kubeblocks/version" 48 ) 49 50 var ( 51 // CliDockerNetwork is docker network for k3d cluster when `kbcli playground` 52 // all cluster will be created in this network, so they can communicate with each other 53 CliDockerNetwork = "k3d-kbcli-playground" 54 55 // K3sImage is k3s image repo 56 K3sImage = "rancher/k3s:" + version.K3sImageTag 57 58 // K3dProxyImage is k3d proxy image repo 59 K3dProxyImage = "docker.io/apecloud/k3d-proxy:" + version.K3dVersion 60 61 // K3dFixEnv 62 KBEnvFix fixes.K3DFixEnv = "KB_FIX_MOUNTS" 63 ) 64 65 //go:embed assets/k3d-entrypoint-mount.sh 66 var k3dMountEntrypoint []byte 67 68 // localCloudProvider handles the k3d playground cluster creation and management 69 type localCloudProvider struct { 70 cfg config.ClusterConfig 71 stdout io.Writer 72 stderr io.Writer 73 } 74 75 // localCloudProvider should be an implementation of cloud provider 76 var _ Interface = &localCloudProvider{} 77 78 func init() { 79 if !klog.V(1).Enabled() { 80 // set k3d log level to 'warning' to avoid too much info logs 81 l.Log().SetLevel(logrus.WarnLevel) 82 } 83 } 84 85 func newLocalCloudProvider(stdout, stderr io.Writer) Interface { 86 return &localCloudProvider{ 87 stdout: stdout, 88 stderr: stderr, 89 } 90 } 91 92 func (p *localCloudProvider) Name() string { 93 return Local 94 } 95 96 // CreateK8sCluster creates a local kubernetes cluster using k3d 97 func (p *localCloudProvider) CreateK8sCluster(clusterInfo *K8sClusterInfo) error { 98 var err error 99 100 if p.cfg, err = buildClusterRunConfig(clusterInfo.ClusterName); err != nil { 101 return err 102 } 103 104 if err = setUpK3d(context.Background(), &p.cfg); err != nil { 105 return errors.Wrapf(err, "failed to create k3d cluster %s", clusterInfo.ClusterName) 106 } 107 108 return nil 109 } 110 111 // DeleteK8sCluster removes the k3d cluster 112 func (p *localCloudProvider) DeleteK8sCluster(clusterInfo *K8sClusterInfo) error { 113 var err error 114 if clusterInfo == nil { 115 clusterInfo, err = p.GetClusterInfo() 116 if err != nil { 117 return err 118 } 119 } 120 ctx := context.Background() 121 clusterName := clusterInfo.ClusterName 122 clusters, err := k3dClient.ClusterList(ctx, runtimes.SelectedRuntime) 123 if err != nil { 124 return errors.Wrap(err, "fail to get k3d cluster list") 125 } 126 127 if len(clusters) == 0 { 128 return errors.New("no cluster found") 129 } 130 131 // find cluster that matches the name 132 var cluster *k3d.Cluster 133 for _, c := range clusters { 134 if c.Name == clusterName { 135 cluster = c 136 break 137 } 138 } 139 140 // extra handling to clean up tools nodes 141 defer func() { 142 if nl, err := k3dClient.NodeList(ctx, runtimes.SelectedRuntime); err == nil { 143 toolNode := fmt.Sprintf("k3d-%s-tools", clusterName) 144 for _, n := range nl { 145 if n.Name == toolNode { 146 if err := k3dClient.NodeDelete(ctx, runtimes.SelectedRuntime, n, k3d.NodeDeleteOpts{}); err != nil { 147 fmt.Printf("Delete node %s failed.", toolNode) 148 } 149 break 150 } 151 } 152 } 153 }() 154 155 if cluster == nil { 156 return fmt.Errorf("k3d cluster %s does not exist", clusterName) 157 } 158 159 // delete playground cluster 160 if err = k3dClient.ClusterDelete(ctx, runtimes.SelectedRuntime, cluster, 161 k3d.ClusterDeleteOpts{SkipRegistryCheck: false}); err != nil { 162 return errors.Wrapf(err, "failed to delete playground cluster %s", clusterName) 163 } 164 return nil 165 } 166 167 func (p *localCloudProvider) GetKubeConfig() (string, error) { 168 ctx := context.Background() 169 cluster := &k3d.Cluster{Name: types.K3dClusterName} 170 kubeConfig, err := k3dClient.KubeconfigGet(ctx, runtimes.SelectedRuntime, cluster) 171 if err != nil { 172 return "", err 173 } 174 cfgBytes, err := clientcmd.Write(*kubeConfig) 175 if err != nil { 176 return "", err 177 } 178 179 var ( 180 hostToReplace string 181 cfgStr = string(cfgBytes) 182 ) 183 184 switch { 185 case strings.Contains(cfgStr, "0.0.0.0"): 186 hostToReplace = "0.0.0.0" 187 case strings.Contains(cfgStr, "host.docker.internal"): 188 hostToReplace = "host.docker.internal" 189 default: 190 return "", errors.Wrap(err, "unrecognized k3d kubeconfig format") 191 } 192 193 // replace host config with loop back address 194 return strings.ReplaceAll(cfgStr, hostToReplace, "127.0.0.1"), nil 195 } 196 197 func (p *localCloudProvider) GetClusterInfo() (*K8sClusterInfo, error) { 198 kubeConfig, err := p.GetKubeConfig() 199 if err != nil { 200 return nil, err 201 } 202 return &K8sClusterInfo{ 203 CloudProvider: p.Name(), 204 ClusterName: types.K3dClusterName, 205 KubeConfig: kubeConfig, 206 Region: "", 207 KbcliVersion: version.GetVersion(), 208 }, nil 209 } 210 211 // buildClusterRunConfig returns the run-config for the k3d cluster 212 func buildClusterRunConfig(clusterName string) (config.ClusterConfig, error) { 213 createOpts := buildClusterCreateOpts() 214 cluster, err := buildClusterConfig(clusterName, createOpts) 215 if err != nil { 216 return config.ClusterConfig{}, err 217 } 218 kubeconfigOpts := buildKubeconfigOptions() 219 runConfig := config.ClusterConfig{ 220 Cluster: cluster, 221 ClusterCreateOpts: createOpts, 222 KubeconfigOpts: kubeconfigOpts, 223 } 224 225 return runConfig, nil 226 } 227 228 func buildClusterCreateOpts() k3d.ClusterCreateOpts { 229 clusterCreateOpts := k3d.ClusterCreateOpts{ 230 GlobalLabels: map[string]string{}, 231 GlobalEnv: []string{}, 232 DisableLoadBalancer: false, 233 } 234 235 for k, v := range k3d.DefaultRuntimeLabels { 236 clusterCreateOpts.GlobalLabels[k] = v 237 } 238 239 return clusterCreateOpts 240 } 241 242 func buildClusterConfig(clusterName string, opts k3d.ClusterCreateOpts) (k3d.Cluster, error) { 243 var network = k3d.ClusterNetwork{ 244 Name: CliDockerNetwork, 245 External: false, 246 } 247 248 port, err := findAvailablePort(6444) 249 if err != nil { 250 panic(err) 251 } 252 253 // build opts to access the Kubernetes API 254 kubeAPIOpts := k3d.ExposureOpts{ 255 PortMapping: nat.PortMapping{ 256 Port: k3d.DefaultAPIPort, 257 Binding: nat.PortBinding{ 258 HostIP: k3d.DefaultAPIHost, 259 HostPort: port, 260 }, 261 }, 262 Host: k3d.DefaultAPIHost, 263 } 264 265 // build cluster config 266 clusterConfig := k3d.Cluster{ 267 Name: clusterName, 268 Network: network, 269 KubeAPI: &kubeAPIOpts, 270 } 271 272 // build nodes 273 var nodes []*k3d.Node 274 275 // build load balancer node 276 clusterConfig.ServerLoadBalancer = buildLoadbalancer(clusterConfig, opts) 277 nodes = append(nodes, clusterConfig.ServerLoadBalancer.Node) 278 279 // build k3d node 280 serverNode := k3d.Node{ 281 Name: k3dClient.GenerateNodeName(clusterConfig.Name, k3d.ServerRole, 0), 282 Role: k3d.ServerRole, 283 Image: K3sImage, 284 ServerOpts: k3d.ServerOpts{}, 285 Args: []string{"--disable=metrics-server", "--disable=traefik", "--disable=local-storage"}, 286 } 287 288 nodes = append(nodes, &serverNode) 289 290 clusterConfig.Nodes = nodes 291 clusterConfig.ServerLoadBalancer.Config.Ports[fmt.Sprintf("%s.tcp", k3d.DefaultAPIPort)] = 292 append(clusterConfig.ServerLoadBalancer.Config.Ports[fmt.Sprintf("%s.tcp", k3d.DefaultAPIPort)], serverNode.Name) 293 294 // other configurations 295 portWithFilter, err := buildPortWithFilters() 296 if err != nil { 297 return clusterConfig, errors.Wrap(err, "failed to build http ports") 298 } 299 300 err = k3dClient.TransformPorts(context.Background(), runtimes.SelectedRuntime, &clusterConfig, []config.PortWithNodeFilters{portWithFilter}) 301 if err != nil { 302 return clusterConfig, errors.Wrap(err, "failed to transform ports") 303 } 304 305 return clusterConfig, nil 306 } 307 308 func findAvailablePort(start int) (string, error) { 309 for i := start; i < 65535; i++ { 310 listener, err := net.Listen("tcp", fmt.Sprintf(":%d", i)) 311 if err != nil { 312 continue 313 } 314 util.CloseQuietly(listener) 315 return strconv.Itoa(i), nil 316 } 317 return "", errors.New("can not find any available port") 318 } 319 320 func buildLoadbalancer(cluster k3d.Cluster, opts k3d.ClusterCreateOpts) *k3d.Loadbalancer { 321 lb := k3d.NewLoadbalancer() 322 323 labels := map[string]string{} 324 if opts.GlobalLabels == nil && len(opts.GlobalLabels) == 0 { 325 labels = opts.GlobalLabels 326 } 327 328 lb.Node.Name = fmt.Sprintf("%s-%s-serverlb", k3d.DefaultObjectNamePrefix, cluster.Name) 329 lb.Node.Image = K3dProxyImage 330 lb.Node.Ports = nat.PortMap{ 331 k3d.DefaultAPIPort: []nat.PortBinding{cluster.KubeAPI.Binding}, 332 } 333 lb.Node.Networks = []string{cluster.Network.Name} 334 lb.Node.RuntimeLabels = labels 335 lb.Node.Restart = true 336 337 return lb 338 } 339 340 func buildPortWithFilters() (config.PortWithNodeFilters, error) { 341 var port config.PortWithNodeFilters 342 343 hostPort, err := findAvailablePort(8090) 344 if err != nil { 345 return port, err 346 } 347 port.Port = fmt.Sprintf("%s:80", hostPort) 348 port.NodeFilters = []string{"loadbalancer"} 349 350 return port, nil 351 } 352 353 func buildKubeconfigOptions() config.SimpleConfigOptionsKubeconfig { 354 opts := config.SimpleConfigOptionsKubeconfig{ 355 UpdateDefaultKubeconfig: true, 356 SwitchCurrentContext: true, 357 } 358 return opts 359 } 360 361 func setUpK3d(ctx context.Context, cluster *config.ClusterConfig) error { 362 // add fix Envs 363 if err := os.Setenv(string(KBEnvFix), "1"); err != nil { 364 return err 365 } 366 fixes.FixEnvs = append(fixes.FixEnvs, KBEnvFix) 367 368 l, err := k3dClient.ClusterList(ctx, runtimes.SelectedRuntime) 369 if err != nil { 370 return err 371 } 372 373 if cluster == nil { 374 return errors.New("failed to create cluster") 375 } 376 377 for _, c := range l { 378 if c.Name == cluster.Name { 379 if c, err := k3dClient.ClusterGet(ctx, runtimes.SelectedRuntime, c); err == nil { 380 klog.V(1).Infof("Detected an existing cluster: %s", c.Name) 381 return nil 382 } 383 break 384 } 385 } 386 387 // exec "mount --make-rshared /" to fix csi driver plugins crash 388 cluster.ClusterCreateOpts.NodeHooks = append(cluster.ClusterCreateOpts.NodeHooks, k3d.NodeHook{ 389 Stage: k3d.LifecycleStagePreStart, 390 Action: actions.WriteFileAction{ 391 Runtime: runtimes.SelectedRuntime, 392 Content: k3dMountEntrypoint, 393 Dest: "/bin/k3d-entrypoint-mount.sh", 394 Mode: 0744, 395 Description: "Write entrypoint script for mount shared fix", 396 }, 397 }) 398 399 if err := k3dClient.ClusterRun(ctx, runtimes.SelectedRuntime, cluster); err != nil { 400 return err 401 } 402 403 return nil 404 }