github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/builder/template/component_wrapper.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 template
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"reflect"
    28  	"strings"
    29  
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/controller-runtime/pkg/log"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    37  	"github.com/1aal/kubeblocks/controllers/apps/components"
    38  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    39  	"github.com/1aal/kubeblocks/pkg/configuration/core"
    40  	"github.com/1aal/kubeblocks/pkg/constant"
    41  	"github.com/1aal/kubeblocks/pkg/controller/builder"
    42  	"github.com/1aal/kubeblocks/pkg/controller/component"
    43  	"github.com/1aal/kubeblocks/pkg/controller/factory"
    44  	"github.com/1aal/kubeblocks/pkg/controller/graph"
    45  	"github.com/1aal/kubeblocks/pkg/controller/model"
    46  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    47  	"github.com/1aal/kubeblocks/pkg/generics"
    48  )
    49  
    50  type templateRenderWorkflow struct {
    51  	renderedOpts RenderedOptions
    52  	clusterYaml  string
    53  	localObjects []client.Object
    54  
    55  	clusterDefObj     *appsv1alpha1.ClusterDefinition
    56  	clusterVersionObj *appsv1alpha1.ClusterVersion
    57  
    58  	clusterDefComponents     []appsv1alpha1.ClusterComponentDefinition
    59  	clusterVersionComponents []appsv1alpha1.ClusterComponentVersion
    60  }
    61  
    62  func (w *templateRenderWorkflow) Do(outputDir string) error {
    63  	var err error
    64  	var cluster *appsv1alpha1.Cluster
    65  
    66  	cli := newMockClient(w.localObjects)
    67  	ctx := intctrlutil.RequestCtx{
    68  		Ctx: context.Background(),
    69  		Log: log.Log.WithName("ctool"),
    70  	}
    71  
    72  	if cluster, err = w.createClusterObject(); err != nil {
    73  		return err
    74  	}
    75  	ctx.Log.V(1).Info(fmt.Sprintf("cluster object : %v", cluster))
    76  
    77  	components := w.clusterDefComponents
    78  	if w.renderedOpts.ConfigSpec != "" {
    79  		comp := w.getComponentWithConfigSpec(w.renderedOpts.ConfigSpec)
    80  		if comp == nil {
    81  			return core.MakeError("config spec[%s] is not found", w.renderedOpts.ConfigSpec)
    82  		}
    83  		components = []appsv1alpha1.ClusterComponentDefinition{*comp}
    84  	}
    85  
    86  	for _, component := range components {
    87  		synthesizedComponent, objects, err := generateComponentObjects(w, ctx, cli, component.Name, cluster)
    88  		if err != nil {
    89  			return err
    90  		}
    91  		if len(objects) == 0 {
    92  			continue
    93  		}
    94  		if err := w.dumpRenderedTemplates(outputDir, objects, component, w.renderedOpts.ConfigSpec, synthesizedComponent, cluster); err != nil {
    95  			return err
    96  		}
    97  	}
    98  	return nil
    99  }
   100  
   101  func (w *templateRenderWorkflow) getComponentName(componentType string, cluster *appsv1alpha1.Cluster) (string, error) {
   102  	clusterCompSpec := cluster.Spec.GetDefNameMappingComponents()[componentType]
   103  	if len(clusterCompSpec) == 0 {
   104  		return "", core.MakeError("component[%s] is not defined in cluster definition", componentType)
   105  	}
   106  	return clusterCompSpec[0].Name, nil
   107  }
   108  
   109  func checkTemplateExist[T any](arrs []T, name string) bool {
   110  	for _, a := range arrs {
   111  		v := reflect.ValueOf(a)
   112  		if v.Kind() == reflect.Ptr {
   113  			v = v.Elem()
   114  		}
   115  		v = v.FieldByName("Name")
   116  		if v.Kind() == reflect.String && v.String() == name {
   117  			return true
   118  		}
   119  	}
   120  	return false
   121  }
   122  
   123  func (w *templateRenderWorkflow) getComponentWithConfigSpec(name string) *appsv1alpha1.ClusterComponentDefinition {
   124  	var compMap map[string]*appsv1alpha1.ClusterComponentVersion
   125  
   126  	if w.clusterVersionObj != nil {
   127  		compMap = w.clusterVersionObj.Spec.GetDefNameMappingComponents()
   128  	}
   129  	for _, component := range w.clusterDefComponents {
   130  		if checkTemplateExist(component.ConfigSpecs, name) {
   131  			return &component
   132  		}
   133  		if checkTemplateExist(component.ScriptSpecs, name) {
   134  			return &component
   135  		}
   136  		if compMap != nil && compMap[component.Name] != nil {
   137  			if checkTemplateExist(compMap[component.Name].ConfigSpecs, name) {
   138  				return &component
   139  			}
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  func (w *templateRenderWorkflow) createClusterObject() (*appsv1alpha1.Cluster, error) {
   146  	if w.clusterYaml != "" {
   147  		return CustomizedObjFromYaml(w.clusterYaml, generics.ClusterSignature)
   148  	}
   149  	return mockClusterObject(w.clusterDefObj, w.renderedOpts, w.clusterVersionObj), nil
   150  }
   151  
   152  func (w *templateRenderWorkflow) dumpRenderedTemplates(outputDir string, objects []client.Object, componentDef appsv1alpha1.ClusterComponentDefinition, name string, synthesizedComponent *component.SynthesizedComponent, cluster *appsv1alpha1.Cluster) error {
   153  	fromTemplate := func(component *component.SynthesizedComponent) []appsv1alpha1.ComponentTemplateSpec {
   154  		templates := make([]appsv1alpha1.ComponentTemplateSpec, 0, len(component.ConfigTemplates)+len(component.ScriptTemplates))
   155  		for _, tpl := range component.ConfigTemplates {
   156  			templates = append(templates, tpl.ComponentTemplateSpec)
   157  		}
   158  		templates = append(templates, component.ScriptTemplates...)
   159  		return templates
   160  	}
   161  	foundConfigSpec := func(component *component.SynthesizedComponent, name string) *appsv1alpha1.ComponentConfigSpec {
   162  		for i := range component.ConfigTemplates {
   163  			template := &component.ConfigTemplates[i]
   164  			if template.Name == name {
   165  				return template
   166  			}
   167  		}
   168  		return nil
   169  	}
   170  
   171  	for _, template := range fromTemplate(synthesizedComponent) {
   172  		if name != "" && name != template.Name {
   173  			continue
   174  		}
   175  		comName, _ := w.getComponentName(componentDef.Name, cluster)
   176  		cfgName := core.GetComponentCfgName(cluster.Name, comName, template.Name)
   177  		if err := dumpTemplate(template, outputDir, objects, componentDef.Name, cfgName, foundConfigSpec(synthesizedComponent, template.Name)); err != nil {
   178  			return err
   179  		}
   180  	}
   181  	return nil
   182  }
   183  
   184  func getClusterDefComponents(clusterDefObj *appsv1alpha1.ClusterDefinition, componentName string) []appsv1alpha1.ClusterComponentDefinition {
   185  	if componentName == "" {
   186  		return clusterDefObj.Spec.ComponentDefs
   187  	}
   188  	component := clusterDefObj.GetComponentDefByName(componentName)
   189  	if component == nil {
   190  		return nil
   191  	}
   192  	return []appsv1alpha1.ClusterComponentDefinition{*component}
   193  }
   194  
   195  func getClusterVersionComponents(clusterVersionObj *appsv1alpha1.ClusterVersion, componentName string) []appsv1alpha1.ClusterComponentVersion {
   196  	if clusterVersionObj == nil {
   197  		return nil
   198  	}
   199  	if componentName == "" {
   200  		return clusterVersionObj.Spec.ComponentVersions
   201  	}
   202  	componentMap := clusterVersionObj.Spec.GetDefNameMappingComponents()
   203  	if component, ok := componentMap[componentName]; ok {
   204  		return []appsv1alpha1.ClusterComponentVersion{*component}
   205  	}
   206  	return nil
   207  }
   208  
   209  func NewWorkflowTemplateRender(helmTemplateDir string, opts RenderedOptions, clusterDef, clusterVersion string) (*templateRenderWorkflow, error) {
   210  	foundCVResource := func(allObjects []client.Object) *appsv1alpha1.ClusterVersion {
   211  		cvObj := GetTypedResourceObjectBySignature(allObjects, generics.ClusterVersionSignature,
   212  			func(object client.Object) bool {
   213  				if clusterVersion != "" {
   214  					return object.GetName() == clusterVersion
   215  				}
   216  				return object.GetAnnotations() != nil && object.GetAnnotations()[constant.DefaultClusterVersionAnnotationKey] == "true"
   217  			})
   218  		if clusterVersion == "" && cvObj == nil {
   219  			cvObj = GetTypedResourceObjectBySignature(allObjects, generics.ClusterVersionSignature)
   220  		}
   221  		return cvObj
   222  	}
   223  
   224  	if _, err := os.Stat(helmTemplateDir); err != nil {
   225  		panic("cluster definition yaml file is required")
   226  	}
   227  
   228  	allObjects, err := CreateObjectsFromDirectory(helmTemplateDir)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	clusterDefObj := GetTypedResourceObjectBySignature(allObjects, generics.ClusterDefinitionSignature, WithResourceName(clusterDef))
   234  	if clusterDefObj == nil {
   235  		return nil, core.MakeError("cluster definition object is not found in helm template directory[%s]", helmTemplateDir)
   236  	}
   237  
   238  	// hack apiserver auto filefield
   239  	checkAndFillPortProtocol(clusterDefObj.Spec.ComponentDefs)
   240  
   241  	var cdComponents []appsv1alpha1.ClusterComponentDefinition
   242  	if cdComponents = getClusterDefComponents(clusterDefObj, opts.ComponentName); cdComponents == nil {
   243  		return nil, core.MakeError("component[%s] is not defined in cluster definition", opts.ComponentName)
   244  	}
   245  	clusterVersionObj := foundCVResource(allObjects)
   246  	return &templateRenderWorkflow{
   247  		renderedOpts:             opts,
   248  		clusterDefObj:            clusterDefObj,
   249  		clusterVersionObj:        clusterVersionObj,
   250  		localObjects:             allObjects,
   251  		clusterDefComponents:     cdComponents,
   252  		clusterVersionComponents: getClusterVersionComponents(clusterVersionObj, opts.ComponentName),
   253  	}, nil
   254  }
   255  
   256  func dumpTemplate(template appsv1alpha1.ComponentTemplateSpec, outputDir string, objects []client.Object, componentDefName string, cfgName string, configSpec *appsv1alpha1.ComponentConfigSpec) error {
   257  	output := filepath.Join(outputDir, cfgName)
   258  	fmt.Printf("dump rendering template spec: %s, output directory: %s\n",
   259  		printer.BoldYellow(fmt.Sprintf("%s.%s", componentDefName, template.Name)), output)
   260  
   261  	if err := os.MkdirAll(output, 0755); err != nil {
   262  		return err
   263  	}
   264  
   265  	var ok bool
   266  	var cm *corev1.ConfigMap
   267  	for _, obj := range objects {
   268  		if cm, ok = obj.(*corev1.ConfigMap); !ok || !isTemplateOwner(cm, configSpec, cfgName) {
   269  			continue
   270  		}
   271  		if isTemplateObject(cm, cfgName) {
   272  			for file, val := range cm.Data {
   273  				if err := os.WriteFile(filepath.Join(output, file), []byte(val), 0755); err != nil {
   274  					return err
   275  				}
   276  			}
   277  		}
   278  		if isTemplateEnvFromObject(cm, configSpec, cfgName) {
   279  			val, err := yaml.Marshal(cm)
   280  			if err != nil {
   281  				return err
   282  			}
   283  			yamlFile := fmt.Sprintf("%s.yaml", cm.Name[len(cfgName)+1:])
   284  			if err := os.WriteFile(filepath.Join(output, yamlFile), val, 0755); err != nil {
   285  				return err
   286  			}
   287  		}
   288  	}
   289  	return nil
   290  }
   291  
   292  func isTemplateObject(cm *corev1.ConfigMap, cfgName string) bool {
   293  	return cm.Name == cfgName
   294  }
   295  
   296  func isTemplateEnvFromObject(cm *corev1.ConfigMap, configSpec *appsv1alpha1.ComponentConfigSpec, cfgName string) bool {
   297  	if configSpec == nil || len(configSpec.AsEnvFrom) == 0 || configSpec.ConfigConstraintRef == "" || len(cm.Labels) == 0 {
   298  		return false
   299  	}
   300  	return cm.Labels[constant.CMTemplateNameLabelKey] == configSpec.Name && strings.HasPrefix(cm.Name, cfgName)
   301  }
   302  
   303  func isTemplateOwner(cm *corev1.ConfigMap, configSpec *appsv1alpha1.ComponentConfigSpec, cfgName string) bool {
   304  	return isTemplateObject(cm, cfgName) || isTemplateEnvFromObject(cm, configSpec, cfgName)
   305  }
   306  
   307  func generateComponentObjects(w *templateRenderWorkflow, ctx intctrlutil.RequestCtx, cli *mockClient,
   308  	componentType string, cluster *appsv1alpha1.Cluster) (*component.SynthesizedComponent, []client.Object, error) {
   309  	cmGVK := generics.ToGVK(&corev1.ConfigMap{})
   310  
   311  	objs := make([]client.Object, 0)
   312  	cli.SetResourceHandler(&ResourceHandler{
   313  		Matcher: []ResourceMatcher{func(obj runtime.Object) bool {
   314  			res := obj.(client.Object)
   315  			return generics.ToGVK(res) == cmGVK &&
   316  				res.GetLabels() != nil &&
   317  				res.GetLabels()[constant.CMTemplateNameLabelKey] != ""
   318  		}},
   319  		Handler: func(obj runtime.Object) error {
   320  			objs = append(objs, obj.(client.Object))
   321  			return nil
   322  		},
   323  	})
   324  
   325  	compName, err := w.getComponentName(componentType, cluster)
   326  	if err != nil {
   327  		return nil, nil, err
   328  	}
   329  	dag := graph.NewDAG()
   330  	root := builder.NewReplicatedStateMachineBuilder(cluster.Namespace, fmt.Sprintf("%s-%s", cluster.Name, compName)).GetObject()
   331  	model.NewGraphClient(nil).Root(dag, nil, root, nil)
   332  	component, err := components.NewComponent(ctx, cli, w.clusterDefObj, w.clusterVersionObj, cluster, compName, dag)
   333  	if err != nil {
   334  		return nil, nil, err
   335  	}
   336  	secret := factory.BuildConnCredential(w.clusterDefObj, cluster, component.GetSynthesizedComponent())
   337  	cli.AppendMockObjects(secret)
   338  	if err = component.Create(ctx, cli); err != nil {
   339  		return nil, nil, err
   340  	}
   341  	return component.GetSynthesizedComponent(), objs, nil
   342  }