github.com/oam-dev/kubevela@v1.9.11/pkg/addon/render.go (about) 1 /* 2 Copyright 2022 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package addon 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "path" 24 "strconv" 25 "strings" 26 27 "cuelang.org/go/cue/ast" 28 "cuelang.org/go/cue/build" 29 "cuelang.org/go/cue/parser" 30 "github.com/cue-exp/kubevelafix" 31 "github.com/pkg/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/klog/v2" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 38 "github.com/kubevela/workflow/pkg/cue/model/value" 39 "github.com/kubevela/workflow/pkg/cue/packages" 40 41 common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 42 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 43 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 44 "github.com/oam-dev/kubevela/apis/types" 45 "github.com/oam-dev/kubevela/pkg/cue/process" 46 "github.com/oam-dev/kubevela/pkg/multicluster" 47 "github.com/oam-dev/kubevela/pkg/oam" 48 "github.com/oam-dev/kubevela/pkg/oam/util" 49 addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" 50 verrors "github.com/oam-dev/kubevela/pkg/utils/errors" 51 ) 52 53 const ( 54 specifyAddonClustersTopologyPolicy = "deploy-addon-to-specified-clusters" 55 addonAllClusterPolicy = "deploy-addon-to-all-clusters" 56 renderOutputCuePath = "output" 57 renderAuxiliaryOutputsPath = "outputs" 58 defaultCuePackageHeader = "main" 59 defaultPackageHeader = "package main\n" 60 ) 61 62 type addonCueTemplateRender struct { 63 addon *InstallPackage 64 inputArgs map[string]interface{} 65 contextInfo map[string]interface{} 66 } 67 68 func (a addonCueTemplateRender) formatContext() (string, error) { 69 args := a.inputArgs 70 if args == nil { 71 args = map[string]interface{}{} 72 } 73 contextInfo := a.contextInfo 74 if contextInfo == nil { 75 contextInfo = map[string]interface{}{} 76 } 77 bt, err := json.Marshal(args) 78 if err != nil { 79 return "", err 80 } 81 paramFile := fmt.Sprintf("%s: %s", process.ParameterFieldName, string(bt)) 82 83 var contextFile = strings.Builder{} 84 // user custom parameter but be the first data and generated data should be appended at last 85 // in case the user defined data has packages 86 contextFile.WriteString(a.addon.Parameters + "\n") 87 88 // add metadata of addon into context 89 contextInfo["metadata"] = a.addon.Meta 90 contextJSON, err := json.Marshal(contextInfo) 91 if err != nil { 92 return "", err 93 } 94 contextFile.WriteString(fmt.Sprintf("context: %s\n", string(contextJSON))) 95 // parameter definition 96 contextFile.WriteString(paramFile + "\n") 97 98 return contextFile.String(), nil 99 } 100 101 // This func can be used for addon render component. 102 // Please notice the result will be stored in object parameter, so object must be a pointer type 103 func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object interface{}) error { 104 contextFile, err := a.formatContext() 105 if err != nil { 106 return err 107 } 108 v, err := value.NewValue(contextFile, nil, "") 109 if err != nil { 110 return err 111 } 112 out, err := v.LookupByScript(cueTemplate) 113 if err != nil { 114 return err 115 } 116 outputContent, err := out.LookupValue(path) 117 if err != nil { 118 return err 119 } 120 return outputContent.UnmarshalTo(object) 121 } 122 123 // renderApp will render Application from CUE files 124 func (a addonCueTemplateRender) renderApp() (*v1beta1.Application, []*unstructured.Unstructured, error) { 125 var app v1beta1.Application 126 var outputs = map[string]interface{}{} 127 var res []*unstructured.Unstructured 128 129 contextFile, err := a.formatContext() 130 if err != nil { 131 return nil, nil, errors.Wrap(err, "format context for app render") 132 } 133 contextCue, err := parser.ParseFile("parameter.cue", contextFile, parser.ParseComments) 134 if err != nil { 135 return nil, nil, errors.Wrap(err, "parse parameter context") 136 } 137 if contextCue.PackageName() == "" { 138 contextFile = value.DefaultPackageHeader + contextFile 139 } 140 141 var files = []string{contextFile} 142 for _, cuef := range a.addon.CUETemplates { 143 files = append(files, cuef.Data) 144 } 145 146 // TODO(wonderflow): add package discover to support vela own packages if needed 147 v, err := newValueWithMainAndFiles(a.addon.AppCueTemplate.Data, files, nil, "") 148 if err != nil { 149 return nil, nil, errors.Wrap(err, "load app template with CUE files") 150 } 151 if v.Error() != nil { 152 return nil, nil, errors.Wrap(v.Error(), "load app template with CUE files") 153 } 154 155 outputContent, err := v.LookupValue(renderOutputCuePath) 156 if err != nil { 157 return nil, nil, errors.Wrap(err, "render app from output field from CUE") 158 } 159 err = outputContent.UnmarshalTo(&app) 160 if err != nil { 161 return nil, nil, errors.Wrap(err, "decode app from CUE") 162 } 163 auxiliaryContent, err := v.LookupValue(renderAuxiliaryOutputsPath) 164 if err != nil { 165 // no outputs defined in app template, return normal data 166 if verrors.IsCuePathNotFound(err) { 167 return &app, res, nil 168 } 169 return nil, nil, errors.Wrap(err, "render app from output field from CUE") 170 } 171 172 err = auxiliaryContent.UnmarshalTo(&outputs) 173 if err != nil { 174 return nil, nil, errors.Wrap(err, "decode app from CUE") 175 } 176 for k, o := range outputs { 177 if ao, ok := o.(map[string]interface{}); ok { 178 auxO := &unstructured.Unstructured{Object: ao} 179 auxO.SetLabels(util.MergeMapOverrideWithDst(auxO.GetLabels(), map[string]string{oam.LabelAddonAuxiliaryName: k})) 180 res = append(res, auxO) 181 } 182 } 183 return &app, res, nil 184 } 185 186 // newValueWithMainAndFiles new a value from main and appendix files 187 func newValueWithMainAndFiles(main string, slaveFiles []string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*value.Value, error) { 188 builder := &build.Instance{} 189 190 mainFile, err := parser.ParseFile("main.cue", main, parser.ParseComments) 191 mainFile = kubevelafix.Fix(mainFile).(*ast.File) 192 if err != nil { 193 return nil, errors.Wrap(err, "parse main file") 194 } 195 if mainFile.PackageName() == "" { 196 // add a default package main if not exist 197 mainFile, err = parser.ParseFile("main.cue", defaultPackageHeader+main, parser.ParseComments) 198 if err != nil { 199 return nil, errors.Wrap(err, "parse main file with added package main header") 200 } 201 } 202 for _, opt := range opts { 203 if err := opt(mainFile); err != nil { 204 return nil, errors.Wrap(err, "run option func for main file") 205 } 206 } 207 if err := builder.AddSyntax(mainFile); err != nil { 208 return nil, errors.Wrap(err, "add main file to CUE builder") 209 } 210 211 for idx, sf := range slaveFiles { 212 cueSF, err := parser.ParseFile("sf-"+strconv.Itoa(idx)+".cue", sf, parser.ParseComments) 213 cueSF = kubevelafix.Fix(cueSF).(*ast.File) 214 if err != nil { 215 return nil, errors.Wrap(err, "parse added file "+strconv.Itoa(idx)+" \n"+sf) 216 } 217 if cueSF.PackageName() != mainFile.PackageName() { 218 continue 219 } 220 for _, opt := range opts { 221 if err := opt(cueSF); err != nil { 222 return nil, errors.Wrap(err, "run option func for files") 223 } 224 } 225 if err := builder.AddSyntax(cueSF); err != nil { 226 return nil, errors.Wrap(err, "add slave files to CUE builder") 227 } 228 } 229 return value.NewValueWithInstance(builder, pd, tagTempl) 230 } 231 232 // generateAppFramework generate application from yaml defined by template.yaml or cue file from template.cue 233 func generateAppFramework(addon *InstallPackage, parameters map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) { 234 if len(addon.AppCueTemplate.Data) != 0 && addon.AppTemplate != nil { 235 return nil, nil, ErrBothCueAndYamlTmpl 236 } 237 238 var app *v1beta1.Application 239 var auxiliaryObjects []*unstructured.Unstructured 240 var err error 241 if len(addon.AppCueTemplate.Data) != 0 { 242 app, auxiliaryObjects, err = renderAppAccordingToCueTemplate(addon, parameters) 243 if err != nil { 244 return nil, nil, err 245 } 246 } else { 247 app = addon.AppTemplate 248 if app == nil { 249 app = &v1beta1.Application{ 250 TypeMeta: metav1.TypeMeta{APIVersion: v1beta1.SchemeGroupVersion.String(), Kind: v1beta1.ApplicationKind}, 251 } 252 } 253 if app.Spec.Components == nil { 254 app.Spec.Components = []common2.ApplicationComponent{} 255 } 256 } 257 258 if app.Name != "" && app.Name != addonutil.Addon2AppName(addon.Name) { 259 klog.Warningf("Application name %s will be overwritten with %s. Consider removing metadata.name in template.", app.Name, addonutil.Addon2AppName(addon.Name)) 260 } 261 app.SetName(addonutil.Addon2AppName(addon.Name)) 262 263 if app.Namespace != "" && app.Namespace != types.DefaultKubeVelaNS { 264 klog.Warningf("Namespace %s will be overwritten with %s. Consider removing metadata.namespace in template.", app.Namespace, types.DefaultKubeVelaNS) 265 } 266 // force override the namespace defined vela with DefaultVelaNS. This value can be modified by env 267 app.SetNamespace(types.DefaultKubeVelaNS) 268 269 if app.Labels == nil { 270 app.Labels = make(map[string]string) 271 } 272 app.Labels[oam.LabelAddonName] = addon.Name 273 app.Labels[oam.LabelAddonVersion] = addon.Version 274 275 for _, aux := range auxiliaryObjects { 276 aux.SetLabels(util.MergeMapOverrideWithDst(aux.GetLabels(), map[string]string{oam.LabelAddonName: addon.Name, oam.LabelAddonVersion: addon.Version})) 277 } 278 279 return app, auxiliaryObjects, nil 280 } 281 282 func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) { 283 r := addonCueTemplateRender{ 284 addon: addon, 285 inputArgs: args, 286 } 287 return r.renderApp() 288 } 289 290 // renderCompAccordingCUETemplate will return a component from cue template 291 func renderCompAccordingCUETemplate(cueTemplate ElementFile, addon *InstallPackage, args map[string]interface{}) (*common2.ApplicationComponent, error) { 292 comp := common2.ApplicationComponent{} 293 294 r := addonCueTemplateRender{ 295 addon: addon, 296 inputArgs: args, 297 } 298 if err := r.toObject(cueTemplate.Data, renderOutputCuePath, &comp); err != nil { 299 return nil, fmt.Errorf("error rendering file %s: %w", cueTemplate.Name, err) 300 } 301 // If the name of component has been set, just keep it, otherwise will set with file name. 302 if len(comp.Name) == 0 { 303 fileName := strings.ReplaceAll(cueTemplate.Name, path.Ext(cueTemplate.Name), "") 304 comp.Name = strings.ReplaceAll(fileName, ".", "-") 305 } 306 return &comp, nil 307 } 308 309 // RenderApp render a K8s application 310 func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Client, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) { 311 if args == nil { 312 args = map[string]interface{}{} 313 } 314 app, auxiliaryObjects, err := generateAppFramework(addon, args) 315 if err != nil { 316 return nil, nil, err 317 } 318 app.Spec.Components = append(app.Spec.Components, renderNeededNamespaceAsComps(addon)...) 319 320 resources, err := renderResources(addon, args) 321 if err != nil { 322 return nil, nil, err 323 } 324 app.Spec.Components = append(app.Spec.Components, resources...) 325 326 // for legacy addons those hasn't define policy in template.cue but still want to deploy runtime cluster 327 // attach topology policy to application. 328 if checkNeedAttachTopologyPolicy(app, addon) { 329 if err := attachPolicyForLegacyAddon(ctx, app, addon, args, k8sClient); err != nil { 330 return nil, nil, err 331 } 332 } 333 return app, auxiliaryObjects, nil 334 } 335 336 func attachPolicyForLegacyAddon(ctx context.Context, app *v1beta1.Application, addon *InstallPackage, args map[string]interface{}, k8sClient client.Client) error { 337 deployClusters, err := checkDeployClusters(ctx, k8sClient, args) 338 if err != nil { 339 return err 340 } 341 342 if !isDeployToRuntime(addon) { 343 return nil 344 } 345 346 if len(deployClusters) == 0 { 347 // empty cluster args deploy to all clusters 348 clusterSelector := map[string]interface{}{ 349 // empty labelSelector means deploy resources to all clusters 350 ClusterLabelSelector: map[string]string{}, 351 } 352 properties, err := json.Marshal(clusterSelector) 353 if err != nil { 354 return err 355 } 356 policy := v1beta1.AppPolicy{ 357 Name: addonAllClusterPolicy, 358 Type: v1alpha1.TopologyPolicyType, 359 Properties: &runtime.RawExtension{Raw: properties}, 360 } 361 app.Spec.Policies = append(app.Spec.Policies, policy) 362 } else { 363 var found bool 364 for _, c := range deployClusters { 365 if c == multicluster.ClusterLocalName { 366 found = true 367 break 368 } 369 } 370 if !found { 371 deployClusters = append(deployClusters, multicluster.ClusterLocalName) 372 } 373 // deploy to specified clusters 374 if app.Spec.Policies == nil { 375 app.Spec.Policies = []v1beta1.AppPolicy{} 376 } 377 body, err := json.Marshal(map[string][]string{types.ClustersArg: deployClusters}) 378 if err != nil { 379 return err 380 } 381 app.Spec.Policies = append(app.Spec.Policies, v1beta1.AppPolicy{ 382 Name: specifyAddonClustersTopologyPolicy, 383 Type: v1alpha1.TopologyPolicyType, 384 Properties: &runtime.RawExtension{Raw: body}, 385 }) 386 } 387 388 return nil 389 } 390 391 func renderResources(addon *InstallPackage, args map[string]interface{}) ([]common2.ApplicationComponent, error) { 392 var resources []common2.ApplicationComponent 393 if len(addon.YAMLTemplates) != 0 { 394 comp, err := renderK8sObjectsComponent(addon.YAMLTemplates, addon.Name) 395 if err != nil { 396 return nil, errors.Wrapf(err, "render components from yaml template") 397 } 398 resources = append(resources, *comp) 399 } 400 401 for _, tmpl := range addon.CUETemplates { 402 isMainCueTemplate, err := checkCueFileHasPackageHeader(tmpl) 403 if err != nil { 404 return nil, err 405 } 406 if isMainCueTemplate { 407 continue 408 } 409 comp, err := renderCompAccordingCUETemplate(tmpl, addon, args) 410 if err != nil && strings.Contains(err.Error(), "var(path=output) not exist") { 411 continue 412 } 413 if err != nil { 414 return nil, NewAddonError(fmt.Sprintf("fail to render cue template %s", err.Error())) 415 } 416 resources = append(resources, *comp) 417 } 418 return resources, nil 419 } 420 421 // checkNeedAttachTopologyPolicy will check this addon want to deploy to runtime-cluster, but application template doesn't specify the 422 // topology policy, then will attach the policy to application automatically. 423 func checkNeedAttachTopologyPolicy(app *v1beta1.Application, addon *InstallPackage) bool { 424 // the cue template will not be attached topology policy for the white-box principle 425 if len(addon.AppCueTemplate.Data) != 0 { 426 return false 427 } 428 if !isDeployToRuntime(addon) { 429 return false 430 } 431 for _, policy := range app.Spec.Policies { 432 if policy.Type == v1alpha1.TopologyPolicyType { 433 klog.Warningf("deployTo in metadata will NOT have any effect. It conflicts with %s policy named %s. Consider removing deployTo field in addon metadata.", v1alpha1.TopologyPolicyType, policy.Name) 434 return false 435 } 436 } 437 return true 438 } 439 440 func isDeployToRuntime(addon *InstallPackage) bool { 441 if addon.DeployTo == nil { 442 return false 443 } 444 return addon.DeployTo.RuntimeCluster || addon.DeployTo.LegacyRuntimeCluster 445 } 446 447 func checkCueFileHasPackageHeader(cueTemplate ElementFile) (bool, error) { 448 cueFile, err := parser.ParseFile(cueTemplate.Name, cueTemplate.Data, parser.ParseComments) 449 if err != nil { 450 return false, err 451 } 452 if cueFile.PackageName() == defaultCuePackageHeader { 453 return true, nil 454 } 455 return false, nil 456 }