github.com/oam-dev/kubevela@v1.9.11/pkg/config/factory.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 config 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "strings" 25 "time" 26 27 "cuelang.org/go/cue" 28 "github.com/getkin/kin-openapi/openapi3" 29 v1 "k8s.io/api/core/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/labels" 34 "k8s.io/apimachinery/pkg/runtime" 35 pkgtypes "k8s.io/apimachinery/pkg/types" 36 "k8s.io/klog/v2" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 "sigs.k8s.io/yaml" 39 40 workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" 41 42 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 43 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 44 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 45 "github.com/oam-dev/kubevela/apis/types" 46 icontext "github.com/oam-dev/kubevela/pkg/config/context" 47 "github.com/oam-dev/kubevela/pkg/config/writer" 48 velacue "github.com/oam-dev/kubevela/pkg/cue" 49 "github.com/oam-dev/kubevela/pkg/cue/script" 50 "github.com/oam-dev/kubevela/pkg/oam" 51 "github.com/oam-dev/kubevela/pkg/oam/util" 52 "github.com/oam-dev/kubevela/pkg/utils/apply" 53 ) 54 55 // SaveInputPropertiesKey define the key name for saving the input properties in the secret. 56 const SaveInputPropertiesKey = "input-properties" 57 58 // SaveObjectReferenceKey define the key name for saving the outputs objects reference metadata in the secret. 59 const SaveObjectReferenceKey = "objects-reference" 60 61 // SaveExpandedWriterKey define the key name for saving the expanded writer config 62 const SaveExpandedWriterKey = "expanded-writer" 63 64 // SaveSchemaKey define the key name for saving the API schema 65 const SaveSchemaKey = "schema" 66 67 // SaveTemplateKey define the key name for saving the config-template 68 const SaveTemplateKey = "template" 69 70 // TemplateConfigMapNamePrefix the prefix of the configmap name. 71 const TemplateConfigMapNamePrefix = "config-template-" 72 73 // TemplateValidationReturns define the key name for the config-template validation returns 74 const TemplateValidationReturns = SaveTemplateKey + ".validation.$returns" 75 76 // TemplateOutput define the key name for the config-template output 77 const TemplateOutput = SaveTemplateKey + ".output" 78 79 // TemplateOutputs define the key name for the config-template outputs 80 const TemplateOutputs = SaveTemplateKey + ".outputs" 81 82 // ErrSensitiveConfig means this config can not be read directly. 83 var ErrSensitiveConfig = errors.New("the config is sensitive") 84 85 // ErrNoConfigOrTarget means the config or the target is empty. 86 var ErrNoConfigOrTarget = errors.New("you must specify the config name and destination to distribute") 87 88 // ErrNotFoundDistribution means the app of the distribution does not exist. 89 var ErrNotFoundDistribution = errors.New("the distribution does not found") 90 91 // ErrConfigExist means the config does exist. 92 var ErrConfigExist = errors.New("the config does exist") 93 94 // ErrConfigNotFound means the config does not exist 95 var ErrConfigNotFound = errors.New("the config does not exist") 96 97 // ErrTemplateNotFound means the template does not exist 98 var ErrTemplateNotFound = errors.New("the template does not exist") 99 100 // ErrChangeTemplate means the template of the config can not be changed 101 var ErrChangeTemplate = errors.New("the template of the config can not be changed") 102 103 // ErrChangeSecretType means the secret type of the config can not be changed 104 var ErrChangeSecretType = errors.New("the secret type of the config can not be changed") 105 106 // NamespacedName the namespace and name model 107 type NamespacedName struct { 108 Name string `json:"name"` 109 Namespace string `json:"namespace"` 110 } 111 112 // Template This is the spec of the config template, parse from the cue script. 113 type Template struct { 114 NamespacedName 115 Alias string `json:"alias,omitempty"` 116 Description string `json:"description,omitempty"` 117 // Scope defines the usage scope of the configuration template. Provides two options: System or Namespace 118 // System: The system users could use this template, and the config secret will save in the vela-system namespace. 119 // Namespace: The config secret will save in the target namespace, such as this namespace belonging to one project. 120 Scope string `json:"scope"` 121 // Sensitive means this config can not be read from the API or the workflow step, only support the safe way, such as Secret. 122 Sensitive bool `json:"sensitive"` 123 124 CreateTime time.Time `json:"createTime"` 125 126 Template script.CUE `json:"template"` 127 128 ExpandedWriter writer.ExpandedWriterConfig `json:"expandedWriter"` 129 130 Schema *openapi3.Schema `json:"schema"` 131 132 ConfigMap *v1.ConfigMap `json:"-"` 133 } 134 135 // Metadata users should provide this model. 136 type Metadata struct { 137 NamespacedName 138 Alias string `json:"alias,omitempty"` 139 Description string `json:"description,omitempty"` 140 Properties map[string]interface{} `json:"properties"` 141 } 142 143 // TemplateMetadata This is the metadata of the config template 144 type TemplateMetadata struct { 145 Name string `json:"name"` 146 Alias string `json:"alias,omitempty"` 147 Description string `json:"description,omitempty"` 148 Sensitive bool `json:"sensitive,omitempty"` 149 Scope string `json:"scope,omitempty"` 150 } 151 152 // Config this is the config model, generated from the template and properties. 153 type Config struct { 154 Metadata 155 CreateTime time.Time 156 Template Template `json:"template"` 157 // Secret this is default output way. 158 Secret *v1.Secret `json:"secret"` 159 160 // ExpandedWriterData 161 ExpandedWriterData *writer.ExpandedWriterData `json:"expandedWriterData"` 162 163 // OutputObjects this means users could define other objects. 164 // This field assign value only on config render stage. 165 OutputObjects map[string]*unstructured.Unstructured 166 167 // ObjectReferences correspond OutputObjects 168 ObjectReferences []v1.ObjectReference 169 170 Targets []*ClusterTargetStatus 171 } 172 173 // ClusterTargetStatus merge the status of the distribution 174 type ClusterTargetStatus struct { 175 ClusterTarget 176 Status string `json:"status"` 177 Application NamespacedName `json:"application"` 178 Message string `json:"message"` 179 } 180 181 // ClusterTarget kubernetes delivery target 182 type ClusterTarget struct { 183 ClusterName string `json:"clusterName"` 184 Namespace string `json:"namespace"` 185 } 186 187 // Distribution the config distribution model 188 type Distribution struct { 189 Name string `json:"name"` 190 Namespace string `json:"namespace"` 191 CreatedTime time.Time `json:"createdTime"` 192 Configs []*NamespacedName `json:"configs"` 193 Targets []*ClusterTarget `json:"targets"` 194 Application pkgtypes.NamespacedName `json:"application"` 195 Status common.AppStatus `json:"status"` 196 } 197 198 // CreateDistributionSpec the spec of the distribution 199 type CreateDistributionSpec struct { 200 Configs []*NamespacedName `json:"configs"` 201 Targets []*ClusterTarget `json:"targets"` 202 } 203 204 // Validation the response of the validation 205 type Validation struct { 206 Result bool `json:"result"` 207 Message string `json:"message"` 208 } 209 210 // Error return the error message 211 func (e *Validation) Error() string { 212 return fmt.Sprintf("failed to validate config: %s", e.Message) 213 } 214 215 // Factory handle the config 216 type Factory interface { 217 ParseTemplate(defaultName string, content []byte) (*Template, error) 218 ParseConfig(ctx context.Context, template NamespacedName, meta Metadata) (*Config, error) 219 220 LoadTemplate(ctx context.Context, name, ns string) (*Template, error) 221 CreateOrUpdateConfigTemplate(ctx context.Context, ns string, it *Template) error 222 DeleteTemplate(ctx context.Context, ns, name string) error 223 ListTemplates(ctx context.Context, ns, scope string) ([]*Template, error) 224 225 ReadConfig(ctx context.Context, namespace, name string) (map[string]interface{}, error) 226 GetConfig(ctx context.Context, namespace, name string, withStatus bool) (*Config, error) 227 ListConfigs(ctx context.Context, namespace, template, scope string, withStatus bool) ([]*Config, error) 228 DeleteConfig(ctx context.Context, namespace, name string) error 229 CreateOrUpdateConfig(ctx context.Context, i *Config, ns string) error 230 IsExist(ctx context.Context, namespace, name string) (bool, error) 231 232 CreateOrUpdateDistribution(ctx context.Context, ns, name string, ads *CreateDistributionSpec) error 233 ListDistributions(ctx context.Context, ns string) ([]*Distribution, error) 234 DeleteDistribution(ctx context.Context, ns, name string) error 235 MergeDistributionStatus(ctx context.Context, config *Config, namespace string) error 236 } 237 238 // Dispatcher is a client for apply resources. 239 type Dispatcher func(context.Context, []*unstructured.Unstructured, []apply.ApplyOption) error 240 241 // NewConfigFactory create a config factory instance 242 func NewConfigFactory(cli client.Client) Factory { 243 return &kubeConfigFactory{cli: cli, apiApply: defaultDispatcher(cli)} 244 } 245 246 // NewConfigFactoryWithDispatcher create a config factory instance with a specified dispatcher 247 func NewConfigFactoryWithDispatcher(cli client.Client, ds Dispatcher) Factory { 248 if ds == nil { 249 ds = defaultDispatcher(cli) 250 } 251 return &kubeConfigFactory{cli: cli, apiApply: ds} 252 } 253 254 func defaultDispatcher(cli client.Client) Dispatcher { 255 api := apply.NewAPIApplicator(cli) 256 return func(ctx context.Context, manifests []*unstructured.Unstructured, ao []apply.ApplyOption) error { 257 for _, m := range manifests { 258 if err := api.Apply(ctx, m, ao...); err != nil { 259 return err 260 } 261 } 262 return nil 263 } 264 } 265 266 type kubeConfigFactory struct { 267 cli client.Client 268 apiApply Dispatcher 269 } 270 271 // ParseTemplate parse a config template instance form the cue script 272 func (k *kubeConfigFactory) ParseTemplate(defaultName string, content []byte) (*Template, error) { 273 cueScript := script.BuildCUEScriptWithDefaultContext(icontext.DefaultContext, content) 274 value, err := cueScript.ParseToTemplateValueWithCueX() 275 if err != nil { 276 return nil, fmt.Errorf("the cue script is invalid:%w", err) 277 } 278 // Render the metadata 279 tm := TemplateMetadata{} 280 metadata := value.LookupPath(cue.ParsePath("metadata")) 281 if !metadata.Exists() && defaultName == "" { 282 return nil, fmt.Errorf("failed to lookup value: var(path=metadata) not exist") 283 } 284 if err := metadata.Decode(&tm); err != nil { 285 return nil, fmt.Errorf("failed to decode the template metadata: %w", err) 286 } 287 if defaultName != "" { 288 tm.Name = defaultName 289 } 290 291 templateValue := value.LookupPath(cue.ParsePath("template")) 292 if !templateValue.Exists() { 293 return nil, fmt.Errorf("failed to lookup value: var(path=template) not exist") 294 } 295 schema, err := cueScript.ParsePropertiesToSchemaWithCueX("template") 296 if err != nil { 297 return nil, fmt.Errorf("the properties of the cue script is invalid:%w", err) 298 } 299 template := &Template{ 300 NamespacedName: NamespacedName{ 301 Name: tm.Name, 302 }, 303 Alias: tm.Alias, 304 Scope: tm.Scope, 305 Sensitive: tm.Sensitive, 306 Template: cueScript, 307 Schema: schema, 308 ExpandedWriter: writer.ParseExpandedWriterConfig(templateValue), 309 } 310 311 var configmap v1.ConfigMap 312 configmap.Name = TemplateConfigMapNamePrefix + template.Name 313 314 configmap.Data = map[string]string{ 315 SaveTemplateKey: string(template.Template), 316 } 317 if template.Schema != nil { 318 data, err := yaml.Marshal(template.Schema) 319 if err != nil { 320 return nil, err 321 } 322 configmap.Data[SaveSchemaKey] = string(data) 323 } 324 data, err := yaml.Marshal(template.ExpandedWriter) 325 if err != nil { 326 return nil, err 327 } 328 configmap.Data[SaveExpandedWriterKey] = string(data) 329 configmap.Labels = map[string]string{ 330 types.LabelConfigCatalog: types.VelaCoreConfig, 331 types.LabelConfigScope: template.Scope, 332 } 333 configmap.Annotations = map[string]string{ 334 types.AnnotationConfigDescription: template.Description, 335 types.AnnotationConfigAlias: template.Alias, 336 types.AnnotationConfigSensitive: fmt.Sprintf("%t", template.Sensitive), 337 } 338 template.ConfigMap = &configmap 339 340 return template, nil 341 } 342 343 // IsFieldNotExist check whether the error type is the field not found 344 func IsFieldNotExist(err error) bool { 345 return strings.Contains(err.Error(), "not exist") 346 } 347 348 // CreateOrUpdateConfigTemplate parse and update the config template 349 func (k *kubeConfigFactory) CreateOrUpdateConfigTemplate(ctx context.Context, ns string, it *Template) error { 350 if ns != "" { 351 it.ConfigMap.Namespace = ns 352 } 353 obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(it.ConfigMap) 354 if err != nil { 355 return fmt.Errorf("fail to convert configmap to unstructured: %w", err) 356 } 357 us := &unstructured.Unstructured{Object: obj} 358 us.SetAPIVersion("v1") 359 us.SetKind("ConfigMap") 360 return k.apiApply(ctx, []*unstructured.Unstructured{us}, []apply.ApplyOption{apply.DisableUpdateAnnotation(), apply.Quiet()}) 361 } 362 363 func convertConfigMap2Template(cm v1.ConfigMap) (*Template, error) { 364 if cm.Labels == nil || cm.Annotations == nil { 365 return nil, fmt.Errorf("this configmap is not a valid config-template") 366 } 367 it := &Template{ 368 NamespacedName: NamespacedName{ 369 Name: strings.Replace(cm.Name, TemplateConfigMapNamePrefix, "", 1), 370 Namespace: cm.Namespace, 371 }, 372 Alias: cm.Annotations[types.AnnotationConfigAlias], 373 Description: cm.Annotations[types.AnnotationConfigDescription], 374 Sensitive: cm.Annotations[types.AnnotationConfigSensitive] == "true", 375 Scope: cm.Labels[types.LabelConfigScope], 376 CreateTime: cm.CreationTimestamp.Time, 377 Template: script.CUE(cm.Data[SaveTemplateKey]), 378 } 379 if cm.Data[SaveSchemaKey] != "" { 380 var schema openapi3.Schema 381 err := yaml.Unmarshal([]byte(cm.Data[SaveSchemaKey]), &schema) 382 if err != nil { 383 return nil, fmt.Errorf("fail to parse the schema: %w", err) 384 } 385 it.Schema = &schema 386 } 387 if cm.Data[SaveExpandedWriterKey] != "" { 388 var config writer.ExpandedWriterConfig 389 err := yaml.Unmarshal([]byte(cm.Data[SaveExpandedWriterKey]), &config) 390 if err != nil { 391 return nil, fmt.Errorf("fail to parse the schema: %w", err) 392 } 393 it.ExpandedWriter = config 394 } 395 return it, nil 396 } 397 398 // DeleteTemplate delete the config template 399 func (k *kubeConfigFactory) DeleteTemplate(ctx context.Context, ns, name string) error { 400 var configmap v1.ConfigMap 401 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: ns, Name: TemplateConfigMapNamePrefix + name}, &configmap); err != nil { 402 if apierrors.IsNotFound(err) { 403 return fmt.Errorf("the config template %s not found", name) 404 } 405 return fmt.Errorf("fail to delete the config template %s:%w", name, err) 406 } 407 return k.cli.Delete(ctx, &configmap) 408 } 409 410 // ListTemplates list the config templates 411 func (k *kubeConfigFactory) ListTemplates(ctx context.Context, ns, scope string) ([]*Template, error) { 412 var list = &v1.ConfigMapList{} 413 selector, err := labels.Parse(fmt.Sprintf("%s=%s", types.LabelConfigCatalog, types.VelaCoreConfig)) 414 if err != nil { 415 return nil, err 416 } 417 if err := k.cli.List(ctx, list, 418 client.MatchingLabelsSelector{Selector: selector}, 419 client.InNamespace(ns)); err != nil { 420 return nil, err 421 } 422 var templates []*Template 423 for _, item := range list.Items { 424 it, err := convertConfigMap2Template(item) 425 if err != nil { 426 klog.Warningf("fail to parse the configmap %s:%s", item.Name, err.Error()) 427 } 428 if it != nil { 429 if scope == "" || it.Scope == scope { 430 templates = append(templates, it) 431 } 432 } 433 } 434 return templates, nil 435 } 436 437 // LoadTemplate load the template 438 func (k *kubeConfigFactory) LoadTemplate(ctx context.Context, name, ns string) (*Template, error) { 439 var cm v1.ConfigMap 440 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: ns, Name: TemplateConfigMapNamePrefix + name}, &cm); err != nil { 441 if apierrors.IsNotFound(err) { 442 return nil, ErrTemplateNotFound 443 } 444 return nil, err 445 } 446 return convertConfigMap2Template(cm) 447 } 448 449 // ParseConfig merge the properties to template and build a config instance 450 // If the templateName is empty, means creating a secret without the template. 451 func (k *kubeConfigFactory) ParseConfig(ctx context.Context, 452 template NamespacedName, meta Metadata, 453 ) (*Config, error) { 454 var secret v1.Secret 455 456 config := &Config{ 457 Metadata: meta, 458 Secret: &secret, 459 } 460 461 if template.Name != "" { 462 template, err := k.LoadTemplate(ctx, template.Name, template.Namespace) 463 if err != nil { 464 return nil, err 465 } 466 contextValue := icontext.ConfigRenderContext{ 467 Name: meta.Name, 468 Namespace: meta.Namespace, 469 } 470 // Compile the config template 471 val, err := template.Template.RunAndOutputWithCueX(ctx, contextValue, meta.Properties) 472 if err != nil && !velacue.IsFieldNotExist(err) { 473 return nil, err 474 } 475 // Render the validation response and check validation result 476 valid := val.LookupPath(cue.ParsePath(TemplateValidationReturns)) 477 validation := Validation{} 478 if valid.Exists() { 479 if err := valid.Decode(&validation); err != nil { 480 return nil, fmt.Errorf("the validation.$returns format must be validation") 481 } 482 } 483 if len(validation.Message) > 0 { 484 return nil, &validation 485 } 486 // Render the output secret 487 output := val.LookupPath(cue.ParsePath(TemplateOutput)) 488 if output.Exists() { 489 if err := output.Decode(&secret); err != nil { 490 return nil, fmt.Errorf("the output format must be secret") 491 } 492 } 493 if secret.Type == "" { 494 secret.Type = v1.SecretType(fmt.Sprintf("%s/%s", "", template.Name)) 495 } 496 if secret.Labels == nil { 497 secret.Labels = map[string]string{} 498 } 499 secret.Labels[types.LabelConfigCatalog] = types.VelaCoreConfig 500 secret.Labels[types.LabelConfigType] = template.Name 501 secret.Labels[types.LabelConfigType] = template.Name 502 secret.Labels[types.LabelConfigScope] = template.Scope 503 504 if secret.Annotations == nil { 505 secret.Annotations = map[string]string{} 506 } 507 secret.Annotations[types.AnnotationConfigSensitive] = fmt.Sprintf("%t", template.Sensitive) 508 secret.Annotations[types.AnnotationConfigTemplateNamespace] = template.Namespace 509 config.Template = *template 510 511 // Render the expanded writer configuration 512 data, err := writer.RenderForExpandedWriter(template.ExpandedWriter, config.Template.Template, contextValue, meta.Properties) 513 if err != nil { 514 return nil, fmt.Errorf("fail to render the content for the expanded writer:%w ", err) 515 } 516 config.ExpandedWriterData = data 517 518 // Render the outputs objects 519 outputs := val.LookupPath(cue.ParsePath(TemplateOutputs)) 520 if outputs.Exists() { 521 var objects = map[string]interface{}{} 522 if err := outputs.Decode(&objects); err != nil { 523 return nil, fmt.Errorf("the outputs is invalid %w", err) 524 } 525 var objectReferences []v1.ObjectReference 526 config.OutputObjects = make(map[string]*unstructured.Unstructured) 527 for k := range objects { 528 if ob, ok := objects[k].(map[string]interface{}); ok { 529 obj := &unstructured.Unstructured{Object: ob} 530 config.OutputObjects[k] = obj 531 objectReferences = append(objectReferences, v1.ObjectReference{ 532 Kind: obj.GetKind(), 533 Namespace: obj.GetNamespace(), 534 Name: obj.GetName(), 535 APIVersion: obj.GetAPIVersion(), 536 }) 537 } 538 } 539 objectReferenceJSON, err := json.Marshal(objectReferences) 540 if err != nil { 541 return nil, err 542 } 543 if secret.Data == nil { 544 secret.Data = map[string][]byte{} 545 } 546 secret.Data[SaveObjectReferenceKey] = objectReferenceJSON 547 } 548 } else { 549 secret.Labels = map[string]string{ 550 types.LabelConfigCatalog: types.VelaCoreConfig, 551 types.LabelConfigType: "", 552 } 553 secret.Annotations = map[string]string{} 554 } 555 secret.Namespace = meta.Namespace 556 if secret.Name == "" { 557 secret.Name = meta.Name 558 } 559 secret.Annotations[types.AnnotationConfigAlias] = meta.Alias 560 secret.Annotations[types.AnnotationConfigDescription] = meta.Description 561 pp, err := json.Marshal(meta.Properties) 562 if err != nil { 563 return nil, err 564 } 565 if secret.Data == nil { 566 secret.Data = map[string][]byte{} 567 } 568 secret.Data[SaveInputPropertiesKey] = pp 569 570 return config, nil 571 } 572 573 // ReadConfig read the config secret 574 func (k *kubeConfigFactory) ReadConfig(ctx context.Context, namespace, name string) (map[string]interface{}, error) { 575 var secret v1.Secret 576 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil { 577 return nil, err 578 } 579 if secret.Annotations[types.AnnotationConfigSensitive] == "true" { 580 return nil, ErrSensitiveConfig 581 } 582 properties := secret.Data[SaveInputPropertiesKey] 583 var input = map[string]interface{}{} 584 if err := json.Unmarshal(properties, &input); err != nil { 585 return nil, err 586 } 587 return input, nil 588 } 589 590 func (k *kubeConfigFactory) GetConfig(ctx context.Context, namespace, name string, withStatus bool) (*Config, error) { 591 var secret v1.Secret 592 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil { 593 if apierrors.IsNotFound(err) { 594 return nil, ErrConfigNotFound 595 } 596 return nil, err 597 } 598 if secret.Annotations[types.AnnotationConfigSensitive] == "true" { 599 return nil, ErrSensitiveConfig 600 } 601 item, err := convertSecret2Config(&secret) 602 if err != nil { 603 return nil, err 604 } 605 if withStatus { 606 if err := k.MergeDistributionStatus(ctx, item, item.Namespace); err != nil && !errors.Is(err, ErrNotFoundDistribution) { 607 klog.Warningf("fail to merge the status %s:%s", item.Name, err.Error()) 608 } 609 } 610 return item, nil 611 } 612 613 // CreateOrUpdateConfig create or update the config. 614 // Write the expand config to the target server. 615 func (k *kubeConfigFactory) CreateOrUpdateConfig(ctx context.Context, i *Config, _ string) error { 616 var secret v1.Secret 617 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: i.Namespace, Name: i.Name}, &secret); err == nil { 618 if secret.Labels[types.LabelConfigType] != i.Template.Name { 619 return ErrChangeTemplate 620 } 621 if i.Secret.Type != "" && secret.Type != i.Secret.Type { 622 return ErrChangeSecretType 623 } 624 } 625 626 obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(i.Secret) 627 if err != nil { 628 return fmt.Errorf("fail to convert secret to unstructured: %w", err) 629 } 630 us := &unstructured.Unstructured{Object: obj} 631 us.SetAPIVersion("v1") 632 us.SetKind("Secret") 633 634 if err := k.apiApply(ctx, []*unstructured.Unstructured{us}, []apply.ApplyOption{apply.DisableUpdateAnnotation(), apply.Quiet()}); err != nil { 635 return fmt.Errorf("fail to apply the secret: %w", err) 636 } 637 for key, obj := range i.OutputObjects { 638 if err := k.apiApply(ctx, []*unstructured.Unstructured{obj}, []apply.ApplyOption{apply.DisableUpdateAnnotation(), apply.Quiet()}); err != nil { 639 return fmt.Errorf("fail to apply the object %s: %w", key, err) 640 } 641 } 642 readConfig := func(ctx context.Context, namespace, name string) (map[string]interface{}, error) { 643 return k.ReadConfig(ctx, namespace, name) 644 } 645 if i.ExpandedWriterData != nil { 646 if errs := writer.Write(ctx, i.ExpandedWriterData, readConfig); len(errs) > 0 { 647 return errs[0] 648 } 649 } 650 return nil 651 } 652 653 func (k *kubeConfigFactory) IsExist(ctx context.Context, namespace, name string) (bool, error) { 654 var secret v1.Secret 655 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil { 656 if apierrors.IsNotFound(err) { 657 return false, nil 658 } 659 return false, err 660 } 661 return true, nil 662 } 663 664 func (k *kubeConfigFactory) ListConfigs(ctx context.Context, namespace, template, scope string, withStatus bool) ([]*Config, error) { 665 var list = &v1.SecretList{} 666 requirement := fmt.Sprintf("%s=%s", types.LabelConfigCatalog, types.VelaCoreConfig) 667 if template != "" { 668 requirement = fmt.Sprintf("%s,%s=%s", requirement, types.LabelConfigType, template) 669 } 670 if scope != "" { 671 requirement = fmt.Sprintf("%s,%s=%s", requirement, types.LabelConfigScope, scope) 672 } 673 selector, err := labels.Parse(requirement) 674 if err != nil { 675 return nil, err 676 } 677 if err := k.cli.List(ctx, list, 678 client.MatchingLabelsSelector{Selector: selector}, 679 client.InNamespace(namespace)); err != nil { 680 return nil, err 681 } 682 var configs []*Config 683 for i := range list.Items { 684 item := list.Items[i] 685 it, err := convertSecret2Config(&item) 686 if err != nil { 687 klog.Warningf("fail to parse the secret %s:%s", item.Name, err.Error()) 688 } 689 if it != nil { 690 if withStatus { 691 if err := k.MergeDistributionStatus(ctx, it, it.Namespace); err != nil && !errors.Is(err, ErrNotFoundDistribution) { 692 klog.Warningf("fail to merge the status %s:%s", item.Name, err.Error()) 693 } 694 } 695 configs = append(configs, it) 696 } 697 } 698 return configs, nil 699 } 700 701 func (k *kubeConfigFactory) DeleteConfig(ctx context.Context, namespace, name string) error { 702 var secret v1.Secret 703 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil { 704 if apierrors.IsNotFound(err) { 705 return fmt.Errorf("the config %s not found", name) 706 } 707 return fmt.Errorf("fail to delete the config %s:%w", name, err) 708 } 709 if secret.Labels[types.LabelConfigCatalog] != types.VelaCoreConfig { 710 return fmt.Errorf("found a secret but is not a config") 711 } 712 713 if objects, exist := secret.Data[SaveObjectReferenceKey]; exist { 714 var objectReferences []v1.ObjectReference 715 if err := json.Unmarshal(objects, &objectReferences); err != nil { 716 return err 717 } 718 for _, obj := range objectReferences { 719 if err := k.cli.Delete(ctx, convertObjectReference2Unstructured(obj)); err != nil && !apierrors.IsNotFound(err) { 720 return fmt.Errorf("fail to clear the object %s:%w", obj.Name, err) 721 } 722 } 723 } 724 725 return k.cli.Delete(ctx, &secret) 726 } 727 728 func (k *kubeConfigFactory) MergeDistributionStatus(ctx context.Context, config *Config, namespace string) error { 729 app := &v1beta1.Application{} 730 if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: DefaultDistributionName(config.Name)}, app); err != nil { 731 if apierrors.IsNotFound(err) { 732 return ErrNotFoundDistribution 733 } 734 return err 735 } 736 var targets []*ClusterTargetStatus 737 for _, policy := range app.Spec.Policies { 738 if policy.Type == v1alpha1.TopologyPolicyType { 739 status := workflowv1alpha1.WorkflowStepPhasePending 740 message := "" 741 if app.Status.Workflow != nil { 742 for _, step := range app.Status.Workflow.Steps { 743 if policy.Name == strings.Replace(step.Name, "deploy-", "", 1) { 744 status = step.Phase 745 message = step.Message 746 } 747 } 748 } 749 var spec v1alpha1.TopologyPolicySpec 750 if err := json.Unmarshal(policy.Properties.Raw, &spec); err == nil { 751 for _, clu := range spec.Clusters { 752 targets = append(targets, &ClusterTargetStatus{ 753 ClusterTarget: ClusterTarget{ 754 Namespace: spec.Namespace, 755 ClusterName: clu, 756 }, 757 Application: NamespacedName{Name: app.Name, Namespace: app.Namespace}, 758 Status: string(status), 759 Message: message, 760 }) 761 } 762 } 763 } 764 } 765 config.Targets = append(config.Targets, targets...) 766 return nil 767 } 768 769 func (k *kubeConfigFactory) CreateOrUpdateDistribution(ctx context.Context, ns, name string, ads *CreateDistributionSpec) error { 770 policies := convertTarget2TopologyPolicy(ads.Targets) 771 if len(policies) == 0 { 772 return ErrNoConfigOrTarget 773 } 774 // create the share policy 775 shareSpec := v1alpha1.SharedResourcePolicySpec{ 776 Rules: []v1alpha1.SharedResourcePolicyRule{{ 777 Selector: v1alpha1.ResourcePolicyRuleSelector{ 778 CompNames: []string{name}, 779 }, 780 }}, 781 } 782 properties, err := json.Marshal(shareSpec) 783 if err == nil { 784 policies = append(policies, v1beta1.AppPolicy{ 785 Type: v1alpha1.SharedResourcePolicyType, 786 Name: "share-config", 787 Properties: &runtime.RawExtension{ 788 Raw: properties, 789 }, 790 }) 791 } 792 793 var objects []map[string]string 794 for _, s := range ads.Configs { 795 objects = append(objects, map[string]string{ 796 "name": s.Name, 797 "namespace": s.Namespace, 798 "resource": "secret", 799 }) 800 } 801 if len(objects) == 0 { 802 return ErrNoConfigOrTarget 803 } 804 805 objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects}) 806 if err != nil { 807 return err 808 } 809 810 reqByte, err := json.Marshal(ads) 811 if err != nil { 812 return err 813 } 814 815 distribution := &v1beta1.Application{ 816 ObjectMeta: metav1.ObjectMeta{ 817 Name: name, 818 Namespace: ns, 819 Labels: map[string]string{ 820 types.LabelSourceOfTruth: types.FromInner, 821 // This label will override the secret label, then change the catalog of the distributed secrets. 822 types.LabelConfigCatalog: types.CatalogConfigDistribution, 823 }, 824 Annotations: map[string]string{ 825 types.AnnotationConfigDistributionSpec: string(reqByte), 826 oam.AnnotationPublishVersion: util.GenerateVersion("config"), 827 }, 828 }, 829 Spec: v1beta1.ApplicationSpec{ 830 Components: []common.ApplicationComponent{ 831 { 832 Name: name, 833 Type: v1alpha1.RefObjectsComponentType, 834 Properties: &runtime.RawExtension{Raw: objectsBytes}, 835 }, 836 }, 837 Policies: policies, 838 }, 839 } 840 obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(distribution) 841 if err != nil { 842 return fmt.Errorf("fail to convert application to unstructured: %w", err) 843 } 844 us := &unstructured.Unstructured{Object: obj} 845 us.SetAPIVersion(v1beta1.SchemeGroupVersion.String()) 846 us.SetKind(v1beta1.ApplicationKind) 847 848 return k.apiApply(ctx, []*unstructured.Unstructured{us}, []apply.ApplyOption{apply.DisableUpdateAnnotation(), apply.Quiet()}) 849 } 850 851 func (k *kubeConfigFactory) ListDistributions(ctx context.Context, ns string) ([]*Distribution, error) { 852 var apps v1beta1.ApplicationList 853 if err := k.cli.List(ctx, &apps, client.MatchingLabels{ 854 types.LabelSourceOfTruth: types.FromInner, 855 types.LabelConfigCatalog: types.CatalogConfigDistribution, 856 }, client.InNamespace(ns)); err != nil { 857 return nil, err 858 } 859 var list []*Distribution 860 for _, app := range apps.Items { 861 dis := &Distribution{ 862 Name: app.Name, 863 Namespace: app.Namespace, 864 CreatedTime: app.CreationTimestamp.Time, 865 Application: pkgtypes.NamespacedName{ 866 Namespace: app.Namespace, 867 Name: app.Name, 868 }, 869 Status: app.Status, 870 } 871 if spec, ok := app.Annotations[types.AnnotationConfigDistributionSpec]; ok { 872 var req CreateDistributionSpec 873 if err := json.Unmarshal([]byte(spec), &req); err == nil { 874 dis.Targets = req.Targets 875 dis.Configs = req.Configs 876 } 877 } 878 list = append(list, dis) 879 } 880 return list, nil 881 } 882 func (k *kubeConfigFactory) DeleteDistribution(ctx context.Context, ns, name string) error { 883 app := &v1beta1.Application{ 884 ObjectMeta: metav1.ObjectMeta{ 885 Namespace: ns, 886 Name: name, 887 }, 888 } 889 if err := k.cli.Delete(ctx, app); err != nil { 890 if apierrors.IsNotFound(err) { 891 return ErrNotFoundDistribution 892 } 893 return err 894 } 895 return nil 896 } 897 898 func convertTarget2TopologyPolicy(targets []*ClusterTarget) (policies []v1beta1.AppPolicy) { 899 for _, target := range targets { 900 policySpec := v1alpha1.TopologyPolicySpec{ 901 Placement: v1alpha1.Placement{ 902 Clusters: []string{target.ClusterName}, 903 }, 904 Namespace: target.Namespace, 905 } 906 properties, err := json.Marshal(policySpec) 907 if err == nil { 908 policies = append(policies, v1beta1.AppPolicy{ 909 Type: v1alpha1.TopologyPolicyType, 910 Name: fmt.Sprintf("%s-%s", target.ClusterName, target.Namespace), 911 Properties: &runtime.RawExtension{ 912 Raw: properties, 913 }, 914 }) 915 } 916 } 917 return 918 } 919 920 func convertSecret2Config(se *v1.Secret) (*Config, error) { 921 if se == nil || se.Labels == nil { 922 return nil, fmt.Errorf("this secret is not a valid config secret") 923 } 924 config := &Config{ 925 Metadata: Metadata{ 926 NamespacedName: NamespacedName{ 927 Name: se.Name, 928 Namespace: se.Namespace, 929 }, 930 }, 931 CreateTime: se.CreationTimestamp.Time, 932 Secret: se, 933 Template: Template{ 934 NamespacedName: NamespacedName{ 935 Name: se.Labels[types.LabelConfigType], 936 }, 937 }, 938 } 939 if se.Annotations != nil { 940 config.Alias = se.Annotations[types.AnnotationConfigAlias] 941 config.Description = se.Annotations[types.AnnotationConfigDescription] 942 config.Template.Namespace = se.Annotations[types.AnnotationConfigTemplateNamespace] 943 config.Template.Sensitive = se.Annotations[types.AnnotationConfigSensitive] == "true" 944 } 945 if !config.Template.Sensitive && len(se.Data[SaveInputPropertiesKey]) > 0 { 946 var properties = map[string]interface{}{} 947 if err := yaml.Unmarshal(se.Data[SaveInputPropertiesKey], &properties); err != nil { 948 return nil, err 949 } 950 config.Properties = properties 951 } 952 if !config.Template.Sensitive { 953 config.Secret = se 954 } else { 955 seCope := se.DeepCopy() 956 seCope.Data = nil 957 seCope.StringData = nil 958 config.Secret = seCope 959 } 960 if content, ok := se.Data[SaveObjectReferenceKey]; ok { 961 var objectReferences []v1.ObjectReference 962 if err := json.Unmarshal(content, &objectReferences); err != nil { 963 klog.Warningf("the object references are invalid, config:%s", se.Name) 964 } 965 config.ObjectReferences = objectReferences 966 } 967 return config, nil 968 } 969 970 func convertObjectReference2Unstructured(ref v1.ObjectReference) *unstructured.Unstructured { 971 var obj unstructured.Unstructured 972 obj.SetAPIVersion(ref.APIVersion) 973 obj.SetNamespace(ref.Namespace) 974 obj.SetKind(ref.Kind) 975 obj.SetName(ref.Name) 976 return &obj 977 } 978 979 // DefaultDistributionName generate the distribution name by a config name 980 func DefaultDistributionName(configName string) string { 981 return fmt.Sprintf("distribute-%s", configName) 982 }