github.com/oam-dev/kubevela@v1.9.11/pkg/workflow/providers/multicluster/deploy.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 multicluster 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 "sync" 24 25 pkgmaps "github.com/kubevela/pkg/util/maps" 26 "github.com/kubevela/pkg/util/slices" 27 "github.com/kubevela/workflow/pkg/cue/model/value" 28 "github.com/pkg/errors" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/utils/pointer" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 33 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 34 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 35 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 36 "github.com/oam-dev/kubevela/apis/types" 37 "github.com/oam-dev/kubevela/pkg/appfile" 38 "github.com/oam-dev/kubevela/pkg/oam" 39 pkgpolicy "github.com/oam-dev/kubevela/pkg/policy" 40 "github.com/oam-dev/kubevela/pkg/policy/envbinding" 41 "github.com/oam-dev/kubevela/pkg/resourcekeeper" 42 "github.com/oam-dev/kubevela/pkg/utils" 43 velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors" 44 oamProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/oam" 45 ) 46 47 // DeployParameter is the parameter of deploy workflow step 48 type DeployParameter struct { 49 // Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster. 50 Policies []string `json:"policies,omitempty"` 51 // Maximum number of concurrent delivered components. 52 Parallelism int64 `json:"parallelism"` 53 // If set false, this step will apply the components with the terraform workload. 54 IgnoreTerraformComponent bool `json:"ignoreTerraformComponent"` 55 // The policies that embeds in the `deploy` step directly 56 InlinePolicies []v1beta1.AppPolicy `json:"inlinePolicies,omitempty"` 57 } 58 59 // DeployWorkflowStepExecutor executor to run deploy workflow step 60 type DeployWorkflowStepExecutor interface { 61 Deploy(ctx context.Context) (healthy bool, reason string, err error) 62 } 63 64 // NewDeployWorkflowStepExecutor . 65 func NewDeployWorkflowStepExecutor(cli client.Client, af *appfile.Appfile, apply oamProvider.ComponentApply, healthCheck oamProvider.ComponentHealthCheck, renderer oamProvider.WorkloadRenderer, parameter DeployParameter) DeployWorkflowStepExecutor { 66 return &deployWorkflowStepExecutor{ 67 cli: cli, 68 af: af, 69 apply: apply, 70 healthCheck: healthCheck, 71 renderer: renderer, 72 parameter: parameter, 73 } 74 } 75 76 type deployWorkflowStepExecutor struct { 77 cli client.Client 78 af *appfile.Appfile 79 apply oamProvider.ComponentApply 80 healthCheck oamProvider.ComponentHealthCheck 81 renderer oamProvider.WorkloadRenderer 82 parameter DeployParameter 83 } 84 85 // Deploy execute deploy workflow step 86 func (executor *deployWorkflowStepExecutor) Deploy(ctx context.Context) (bool, string, error) { 87 policies, err := selectPolicies(executor.af.Policies, executor.parameter.Policies) 88 if err != nil { 89 return false, "", err 90 } 91 policies = append(policies, fillInlinePolicyNames(executor.parameter.InlinePolicies)...) 92 components, err := loadComponents(ctx, executor.renderer, executor.cli, executor.af, executor.af.Components, executor.parameter.IgnoreTerraformComponent) 93 if err != nil { 94 return false, "", err 95 } 96 97 // Dealing with topology, override and replication policies in order. 98 placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(ctx, executor.cli, executor.af.Namespace, policies, resourcekeeper.AllowCrossNamespaceResource) 99 if err != nil { 100 return false, "", err 101 } 102 components, err = overrideConfiguration(policies, components) 103 if err != nil { 104 return false, "", err 105 } 106 components, err = pkgpolicy.ReplicateComponents(policies, components) 107 if err != nil { 108 return false, "", err 109 } 110 return applyComponents(ctx, executor.apply, executor.healthCheck, components, placements, int(executor.parameter.Parallelism)) 111 } 112 113 func selectPolicies(policies []v1beta1.AppPolicy, policyNames []string) ([]v1beta1.AppPolicy, error) { 114 policyMap := make(map[string]v1beta1.AppPolicy) 115 for _, policy := range policies { 116 policyMap[policy.Name] = policy 117 } 118 var selectedPolicies []v1beta1.AppPolicy 119 for _, policyName := range policyNames { 120 if policy, found := policyMap[policyName]; found { 121 selectedPolicies = append(selectedPolicies, policy) 122 } else { 123 return nil, errors.Errorf("policy %s not found", policyName) 124 } 125 } 126 return selectedPolicies, nil 127 } 128 129 func fillInlinePolicyNames(policies []v1beta1.AppPolicy) []v1beta1.AppPolicy { 130 for i := range policies { 131 if policies[i].Name == "" { 132 policies[i].Name = fmt.Sprintf("inline-%s-policy-%d", policies[i].Type, i) 133 } 134 } 135 return policies 136 } 137 138 func loadComponents(ctx context.Context, renderer oamProvider.WorkloadRenderer, cli client.Client, af *appfile.Appfile, components []common.ApplicationComponent, ignoreTerraformComponent bool) ([]common.ApplicationComponent, error) { 139 var loadedComponents []common.ApplicationComponent 140 for _, comp := range components { 141 loadedComp, err := af.LoadDynamicComponent(ctx, cli, comp.DeepCopy()) 142 if err != nil { 143 return nil, err 144 } 145 if ignoreTerraformComponent { 146 wl, err := renderer(ctx, comp) 147 if err != nil { 148 return nil, errors.Wrapf(err, "failed to render component into workload") 149 } 150 if wl.CapabilityCategory == types.TerraformCategory { 151 continue 152 } 153 } 154 loadedComponents = append(loadedComponents, *loadedComp) 155 } 156 return loadedComponents, nil 157 } 158 159 func overrideConfiguration(policies []v1beta1.AppPolicy, components []common.ApplicationComponent) ([]common.ApplicationComponent, error) { 160 var err error 161 for _, policy := range policies { 162 if policy.Type == v1alpha1.OverridePolicyType { 163 if policy.Properties == nil { 164 return nil, fmt.Errorf("override policy %s must not have empty properties", policy.Name) 165 } 166 overrideSpec := &v1alpha1.OverridePolicySpec{} 167 if err := utils.StrictUnmarshal(policy.Properties.Raw, overrideSpec); err != nil { 168 return nil, errors.Wrapf(err, "failed to parse override policy %s", policy.Name) 169 } 170 components, err = envbinding.PatchComponents(components, overrideSpec.Components, overrideSpec.Selector) 171 if err != nil { 172 return nil, errors.Wrapf(err, "failed to apply override policy %s", policy.Name) 173 } 174 } 175 } 176 return components, nil 177 } 178 179 type valueBuilder func(s string) (*value.Value, error) 180 181 type applyTask struct { 182 component common.ApplicationComponent 183 placement v1alpha1.PlacementDecision 184 healthy *bool 185 } 186 187 func (t *applyTask) key() string { 188 return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, t.component.Name) 189 } 190 191 func (t *applyTask) varKey(v string) string { 192 return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, v) 193 } 194 195 func (t *applyTask) varKeyWithoutReplica(v string) string { 196 return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, "", v) 197 } 198 199 func (t *applyTask) getVar(from string, cache *pkgmaps.SyncMap[string, *value.Value]) *value.Value { 200 key := t.varKey(from) 201 keyWithNoReplica := t.varKeyWithoutReplica(from) 202 var val *value.Value 203 var ok bool 204 if val, ok = cache.Get(key); !ok { 205 if val, ok = cache.Get(keyWithNoReplica); !ok { 206 return nil 207 } 208 } 209 return val 210 } 211 212 func (t *applyTask) fillInputs(inputs *pkgmaps.SyncMap[string, *value.Value], build valueBuilder) error { 213 if len(t.component.Inputs) == 0 { 214 return nil 215 } 216 217 x, err := component2Value(t.component, build) 218 if err != nil { 219 return err 220 } 221 222 for _, input := range t.component.Inputs { 223 var inputVal *value.Value 224 if inputVal = t.getVar(input.From, inputs); inputVal == nil { 225 return fmt.Errorf("input %s is not ready", input) 226 } 227 228 err = x.FillValueByScript(inputVal, fieldPathToComponent(input.ParameterKey)) 229 if err != nil { 230 return errors.Wrap(err, "fill value to component") 231 } 232 } 233 newComp, err := value2Component(x) 234 if err != nil { 235 return err 236 } 237 t.component = *newComp 238 return nil 239 } 240 241 func (t *applyTask) generateOutput(output *unstructured.Unstructured, outputs []*unstructured.Unstructured, cache *pkgmaps.SyncMap[string, *value.Value], build valueBuilder) error { 242 if len(t.component.Outputs) == 0 { 243 return nil 244 } 245 246 var cueString string 247 if output != nil { 248 outputJSON, err := output.MarshalJSON() 249 if err != nil { 250 return errors.Wrap(err, "marshal output") 251 } 252 cueString += fmt.Sprintf("output:%s\n", string(outputJSON)) 253 } 254 componentVal, err := build(cueString) 255 if err != nil { 256 return errors.Wrap(err, "create cue value from component") 257 } 258 259 for _, os := range outputs { 260 name := os.GetLabels()[oam.TraitResource] 261 if name != "" { 262 if err := componentVal.FillObject(os.Object, "outputs", name); err != nil { 263 return errors.WithMessage(err, "FillOutputs") 264 } 265 } 266 } 267 268 for _, o := range t.component.Outputs { 269 pathToSetVar := t.varKey(o.Name) 270 actualOutput, err := componentVal.LookupValue(o.ValueFrom) 271 if err != nil { 272 return errors.Wrap(err, "lookup output") 273 } 274 cache.Set(pathToSetVar, actualOutput) 275 } 276 return nil 277 } 278 279 func (t *applyTask) allDependsReady(healthyMap map[string]bool) bool { 280 for _, d := range t.component.DependsOn { 281 dKey := fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, d) 282 dKeyWithoutReplica := fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, "", d) 283 if !healthyMap[dKey] && !healthyMap[dKeyWithoutReplica] { 284 return false 285 } 286 } 287 return true 288 } 289 290 func (t *applyTask) allInputReady(cache *pkgmaps.SyncMap[string, *value.Value]) bool { 291 for _, in := range t.component.Inputs { 292 if val := t.getVar(in.From, cache); val == nil { 293 return false 294 } 295 } 296 297 return true 298 } 299 300 type applyTaskResult struct { 301 healthy bool 302 err error 303 task *applyTask 304 } 305 306 // applyComponents will apply components to placements. 307 func applyComponents(ctx context.Context, apply oamProvider.ComponentApply, healthCheck oamProvider.ComponentHealthCheck, components []common.ApplicationComponent, placements []v1alpha1.PlacementDecision, parallelism int) (bool, string, error) { 308 var tasks []*applyTask 309 var cache = pkgmaps.NewSyncMap[string, *value.Value]() 310 rootValue, err := value.NewValue("{}", nil, "") 311 if err != nil { 312 return false, "", err 313 } 314 var cueMutex sync.Mutex 315 var makeValue = func(s string) (*value.Value, error) { 316 cueMutex.Lock() 317 defer cueMutex.Unlock() 318 return rootValue.MakeValue(s) 319 } 320 321 taskHealthyMap := map[string]bool{} 322 for _, comp := range components { 323 for _, pl := range placements { 324 tasks = append(tasks, &applyTask{component: comp, placement: pl}) 325 } 326 } 327 unhealthyResults := make([]*applyTaskResult, 0) 328 maxHealthCheckTimes := len(tasks) 329 HealthCheck: 330 for i := 0; i < maxHealthCheckTimes; i++ { 331 checkTasks := make([]*applyTask, 0) 332 for _, task := range tasks { 333 if task.healthy == nil && task.allDependsReady(taskHealthyMap) && task.allInputReady(cache) { 334 task.healthy = new(bool) 335 err := task.fillInputs(cache, makeValue) 336 if err != nil { 337 taskHealthyMap[task.key()] = false 338 unhealthyResults = append(unhealthyResults, &applyTaskResult{healthy: false, err: err, task: task}) 339 continue 340 } 341 checkTasks = append(checkTasks, task) 342 } 343 } 344 if len(checkTasks) == 0 { 345 break HealthCheck 346 } 347 checkResults := slices.ParMap[*applyTask, *applyTaskResult](checkTasks, func(task *applyTask) *applyTaskResult { 348 healthy, output, outputs, err := healthCheck(ctx, task.component, nil, task.placement.Cluster, task.placement.Namespace) 349 task.healthy = pointer.Bool(healthy) 350 if healthy { 351 err = task.generateOutput(output, outputs, cache, makeValue) 352 } 353 return &applyTaskResult{healthy: healthy, err: err, task: task} 354 }, slices.Parallelism(parallelism)) 355 356 for _, res := range checkResults { 357 taskHealthyMap[res.task.key()] = res.healthy 358 if !res.healthy || res.err != nil { 359 unhealthyResults = append(unhealthyResults, res) 360 } 361 } 362 } 363 364 var pendingTasks []*applyTask 365 var todoTasks []*applyTask 366 367 for _, task := range tasks { 368 if healthy, ok := taskHealthyMap[task.key()]; healthy && ok { 369 continue 370 } 371 if task.allDependsReady(taskHealthyMap) && task.allInputReady(cache) { 372 todoTasks = append(todoTasks, task) 373 } else { 374 pendingTasks = append(pendingTasks, task) 375 } 376 } 377 var results []*applyTaskResult 378 if len(todoTasks) > 0 { 379 results = slices.ParMap[*applyTask, *applyTaskResult](todoTasks, func(task *applyTask) *applyTaskResult { 380 err := task.fillInputs(cache, makeValue) 381 if err != nil { 382 return &applyTaskResult{healthy: false, err: err, task: task} 383 } 384 _, _, healthy, err := apply(ctx, task.component, nil, task.placement.Cluster, task.placement.Namespace) 385 if err != nil { 386 return &applyTaskResult{healthy: healthy, err: err, task: task} 387 } 388 return &applyTaskResult{healthy: healthy, err: err, task: task} 389 }, slices.Parallelism(parallelism)) 390 } 391 var errs []error 392 var allHealthy = true 393 var reasons []string 394 for _, res := range unhealthyResults { 395 if res.err != nil { 396 errs = append(errs, fmt.Errorf("error health check from %s: %w", res.task.key(), res.err)) 397 } 398 } 399 for _, res := range results { 400 if res.err != nil { 401 errs = append(errs, fmt.Errorf("error encountered in cluster %s: %w", res.task.placement.Cluster, res.err)) 402 } 403 if !res.healthy { 404 allHealthy = false 405 reasons = append(reasons, fmt.Sprintf("%s is not healthy", res.task.key())) 406 } 407 } 408 409 for _, t := range pendingTasks { 410 reasons = append(reasons, fmt.Sprintf("%s is waiting dependents", t.key())) 411 } 412 413 return allHealthy && len(pendingTasks) == 0, strings.Join(reasons, ","), velaerrors.AggregateErrors(errs) 414 } 415 416 func fieldPathToComponent(input string) string { 417 return fmt.Sprintf("properties.%s", strings.TrimSpace(input)) 418 } 419 420 func component2Value(comp common.ApplicationComponent, build valueBuilder) (*value.Value, error) { 421 x, err := build("") 422 if err != nil { 423 return nil, err 424 } 425 err = x.FillObject(comp, "") 426 if err != nil { 427 return nil, err 428 } 429 // Component.ReplicaKey have no json tag, so we need to set it manually 430 err = x.FillObject(comp.ReplicaKey, "replicaKey") 431 if err != nil { 432 return nil, err 433 } 434 return x, nil 435 } 436 437 func value2Component(v *value.Value) (*common.ApplicationComponent, error) { 438 var comp common.ApplicationComponent 439 err := v.UnmarshalTo(&comp) 440 if err != nil { 441 return nil, err 442 } 443 if rk, err := v.GetString("replicaKey"); err == nil { 444 comp.ReplicaKey = rk 445 } 446 return &comp, nil 447 }