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  }