github.com/oam-dev/kubevela@v1.9.11/pkg/appfile/dryrun/dryrun.go (about)

     1  /*
     2  Copyright 2021 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 dryrun
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	"github.com/pkg/errors"
    28  	corev1 "k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/client-go/discovery"
    31  	"k8s.io/client-go/rest"
    32  	"k8s.io/kubectl/pkg/util/openapi"
    33  	"k8s.io/kubectl/pkg/util/openapi/validation"
    34  	kval "k8s.io/kubectl/pkg/validation"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  	"sigs.k8s.io/yaml"
    37  
    38  	"github.com/kubevela/workflow/pkg/cue/packages"
    39  
    40  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    41  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    42  	"github.com/oam-dev/kubevela/apis/types"
    43  	"github.com/oam-dev/kubevela/pkg/appfile"
    44  	"github.com/oam-dev/kubevela/pkg/cue/definition"
    45  	"github.com/oam-dev/kubevela/pkg/oam"
    46  	oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
    47  	"github.com/oam-dev/kubevela/pkg/policy/envbinding"
    48  	"github.com/oam-dev/kubevela/pkg/utils"
    49  	"github.com/oam-dev/kubevela/pkg/utils/apply"
    50  	"github.com/oam-dev/kubevela/pkg/workflow/step"
    51  )
    52  
    53  // DryRun executes dry-run on an application
    54  type DryRun interface {
    55  	ExecuteDryRun(ctx context.Context, app *v1beta1.Application) ([]*types.ComponentManifest, []*unstructured.Unstructured, error)
    56  }
    57  
    58  // NewDryRunOption creates a dry-run option
    59  func NewDryRunOption(c client.Client, cfg *rest.Config, pd *packages.PackageDiscover, as []*unstructured.Unstructured, serverSideDryRun bool) *Option {
    60  	parser := appfile.NewDryRunApplicationParser(c, pd, as)
    61  	return &Option{c, pd, parser, parser.GenerateAppFileFromApp, cfg, as, serverSideDryRun}
    62  }
    63  
    64  // GenerateAppFileFunc generate the app file model from an application
    65  type GenerateAppFileFunc func(ctx context.Context, app *v1beta1.Application) (*appfile.Appfile, error)
    66  
    67  // Option contains options to execute dry-run
    68  type Option struct {
    69  	Client          client.Client
    70  	PackageDiscover *packages.PackageDiscover
    71  	Parser          *appfile.Parser
    72  	GenerateAppFile GenerateAppFileFunc
    73  	cfg             *rest.Config
    74  	// Auxiliaries are capability definitions used to parse application.
    75  	// DryRun will use capabilities in Auxiliaries as higher priority than
    76  	// getting one from cluster.
    77  	Auxiliaries []*unstructured.Unstructured
    78  
    79  	// serverSideDryRun If set to true, means will dry run via the apiserver.
    80  	serverSideDryRun bool
    81  }
    82  
    83  // validateObjectFromFile will read file into Unstructured object
    84  func (d *Option) validateObjectFromFile(filename string) (*unstructured.Unstructured, error) {
    85  	fileContent, err := os.ReadFile(filepath.Clean(filename))
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	fileType := filepath.Ext(filename)
    91  	switch fileType {
    92  	case ".yaml", ".yml":
    93  		fileContent, err = yaml.YAMLToJSON(fileContent)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  	}
    98  
    99  	dc, err := discovery.NewDiscoveryClientForConfig(d.cfg)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	openAPIGetter := openapi.NewOpenAPIGetter(dc)
   104  	resources, err := openapi.NewOpenAPIParser(openAPIGetter).Parse()
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	valids := kval.ConjunctiveSchema{validation.NewSchemaValidation(resources), kval.NoDoubleKeySchema{}}
   110  	if err = valids.ValidateBytes(fileContent); err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	app := new(unstructured.Unstructured)
   115  	err = json.Unmarshal(fileContent, app)
   116  	return app, err
   117  }
   118  
   119  // ValidateApp will validate app with client schema check and server side dry-run
   120  func (d *Option) ValidateApp(ctx context.Context, filename string) error {
   121  	app, err := d.validateObjectFromFile(filename)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	if len(app.GetNamespace()) == 0 {
   126  		app.SetNamespace(corev1.NamespaceDefault)
   127  	}
   128  	app2 := app.DeepCopy()
   129  
   130  	err = d.Client.Get(ctx, client.ObjectKey{Namespace: app.GetNamespace(), Name: app.GetName()}, app2)
   131  	if err == nil {
   132  		app.SetResourceVersion(app2.GetResourceVersion())
   133  		return d.Client.Update(ctx, app, client.DryRunAll)
   134  	}
   135  	return d.Client.Create(ctx, app, client.DryRunAll)
   136  }
   137  
   138  // ExecuteDryRun simulates applying an application into cluster and returns rendered
   139  // resources but not persist them into cluster.
   140  func (d *Option) ExecuteDryRun(ctx context.Context, application *v1beta1.Application) ([]*types.ComponentManifest, []*unstructured.Unstructured, error) {
   141  	app := application.DeepCopy()
   142  	if app.Namespace != "" {
   143  		ctx = oamutil.SetNamespaceInCtx(ctx, app.Namespace)
   144  	}
   145  	appFile, err := d.GenerateAppFile(ctx, app)
   146  	if err != nil {
   147  		return nil, nil, errors.WithMessage(err, "cannot generate appFile from application")
   148  	}
   149  	if appFile.Namespace == "" {
   150  		appFile.Namespace = corev1.NamespaceDefault
   151  	}
   152  
   153  	comps, err := appFile.GenerateComponentManifests()
   154  	if err != nil {
   155  		return nil, nil, errors.WithMessage(err, "cannot generate manifests from components and traits")
   156  	}
   157  	policyManifests, err := appFile.GeneratePolicyManifests(ctx)
   158  	if err != nil {
   159  		return nil, nil, errors.WithMessage(err, "cannot generate manifests from policies")
   160  	}
   161  	if d.serverSideDryRun {
   162  		applyUtil := apply.NewAPIApplicator(d.Client)
   163  		if err := applyUtil.Apply(ctx, app, apply.DryRunAll()); err != nil {
   164  			return nil, nil, err
   165  		}
   166  	}
   167  	return comps, policyManifests, nil
   168  }
   169  
   170  // PrintDryRun will print the result of dry-run
   171  func (d *Option) PrintDryRun(buff *bytes.Buffer, appName string, comps []*types.ComponentManifest, policies []*unstructured.Unstructured) error {
   172  	var components = make(map[string]*unstructured.Unstructured)
   173  	for _, comp := range comps {
   174  		components[comp.Name] = comp.ComponentOutput
   175  	}
   176  	for _, c := range comps {
   177  		if _, err := fmt.Fprintf(buff, "---\n# Application(%s) -- Component(%s) \n---\n\n", appName, c.Name); err != nil {
   178  			return errors.Wrap(err, "fail to write buff")
   179  		}
   180  		result, err := yaml.Marshal(components[c.Name])
   181  		if err != nil {
   182  			return errors.New("marshal result for component " + c.Name + " object in yaml format")
   183  		}
   184  		buff.Write(result)
   185  		buff.WriteString("\n---\n")
   186  		for _, t := range c.ComponentOutputsAndTraits {
   187  			traitType := t.GetLabels()[oam.TraitTypeLabel]
   188  			switch {
   189  			case traitType == definition.AuxiliaryWorkload:
   190  				buff.WriteString("## From the auxiliary workload \n")
   191  			case traitType != "":
   192  				fmt.Fprintf(buff, "## From the trait %s \n", traitType)
   193  			}
   194  			result, err := yaml.Marshal(t)
   195  			if err != nil {
   196  				return errors.New("marshal result for Component " + c.Name + " trait " + t.GetName() + " object in yaml format")
   197  			}
   198  			buff.Write(result)
   199  			buff.WriteString("\n---\n")
   200  		}
   201  		buff.WriteString("\n")
   202  	}
   203  	for _, plc := range policies {
   204  		if _, err := fmt.Fprintf(buff, "---\n# Application(%s) -- Policy(%s) \n---\n\n", appName, plc.GetName()); err != nil {
   205  			return errors.Wrap(err, "fail to write buff")
   206  		}
   207  		result, err := yaml.Marshal(plc)
   208  		if err != nil {
   209  			return errors.New("marshal result for policy " + plc.GetName() + " object in yaml format")
   210  		}
   211  		buff.Write(result)
   212  		buff.WriteString("\n---\n")
   213  	}
   214  	return nil
   215  }
   216  
   217  // ExecuteDryRunWithPolicies is similar to ExecuteDryRun func, but considers deploy workflow step and topology+override policies
   218  func (d *Option) ExecuteDryRunWithPolicies(ctx context.Context, application *v1beta1.Application, buff *bytes.Buffer) error {
   219  
   220  	app := application.DeepCopy()
   221  	appNs := ctx.Value(oamutil.AppDefinitionNamespace)
   222  	if appNs == nil {
   223  		if app.Namespace == "" {
   224  			app.Namespace = corev1.NamespaceDefault
   225  		}
   226  	} else {
   227  		app.Namespace = appNs.(string)
   228  	}
   229  	ctx = oamutil.SetNamespaceInCtx(ctx, app.Namespace)
   230  	parser := appfile.NewDryRunApplicationParser(d.Client, d.PackageDiscover, d.Auxiliaries)
   231  	af, err := parser.GenerateAppFileFromApp(ctx, app)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	deployWorkflowCount := 0
   236  	for _, wfs := range af.WorkflowSteps {
   237  		if wfs.Type == step.DeployWorkflowStep {
   238  			deployWorkflowCount++
   239  			deployWorkflowStepSpec := &step.DeployWorkflowStepSpec{}
   240  			if err := utils.StrictUnmarshal(wfs.Properties.Raw, deployWorkflowStepSpec); err != nil {
   241  				return err
   242  			}
   243  
   244  			topologyPolicies, overridePolicies, err := filterPolicies(af.Policies, deployWorkflowStepSpec.Policies)
   245  			if err != nil {
   246  				return err
   247  			}
   248  			if len(topologyPolicies) > 0 {
   249  				for _, tp := range topologyPolicies {
   250  					patchedApp, err := patchApp(app, overridePolicies)
   251  					if err != nil {
   252  						return err
   253  					}
   254  					comps, pms, err := d.ExecuteDryRun(ctx, patchedApp)
   255  					if err != nil {
   256  						return err
   257  					}
   258  					err = d.PrintDryRun(buff, fmt.Sprintf("%s with topology %s", patchedApp.Name, tp.Name), comps, pms)
   259  					if err != nil {
   260  						return err
   261  					}
   262  				}
   263  			} else {
   264  				patchedApp, err := patchApp(app, overridePolicies)
   265  				if err != nil {
   266  					return err
   267  				}
   268  				comps, pms, err := d.ExecuteDryRun(ctx, patchedApp)
   269  				if err != nil {
   270  					return err
   271  				}
   272  				err = d.PrintDryRun(buff, fmt.Sprintf("%s only with override policies", patchedApp.Name), comps, pms)
   273  				if err != nil {
   274  					return err
   275  				}
   276  			}
   277  		}
   278  	}
   279  	if deployWorkflowCount == 0 {
   280  		comps, pms, err := d.ExecuteDryRun(ctx, app)
   281  		if err != nil {
   282  			return err
   283  		}
   284  		err = d.PrintDryRun(buff, app.Name, comps, pms)
   285  		if err != nil {
   286  			return err
   287  		}
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func filterPolicies(policies []v1beta1.AppPolicy, policyNames []string) ([]v1beta1.AppPolicy, []v1beta1.AppPolicy, error) {
   294  	policyMap := make(map[string]v1beta1.AppPolicy)
   295  	for _, policy := range policies {
   296  		policyMap[policy.Name] = policy
   297  	}
   298  	var topologyPolicies []v1beta1.AppPolicy
   299  	var overridePolicies []v1beta1.AppPolicy
   300  	for _, policyName := range policyNames {
   301  		if policy, found := policyMap[policyName]; found {
   302  			switch policy.Type {
   303  			case v1alpha1.TopologyPolicyType:
   304  				topologyPolicies = append(topologyPolicies, policy)
   305  			case v1alpha1.OverridePolicyType:
   306  				overridePolicies = append(overridePolicies, policy)
   307  			}
   308  		} else {
   309  			return nil, nil, errors.Errorf("policy %s not found", policyName)
   310  		}
   311  	}
   312  	return topologyPolicies, overridePolicies, nil
   313  }
   314  
   315  func patchApp(application *v1beta1.Application, overridePolicies []v1beta1.AppPolicy) (*v1beta1.Application, error) {
   316  	app := application.DeepCopy()
   317  	for _, policy := range overridePolicies {
   318  
   319  		if policy.Properties == nil {
   320  			return nil, fmt.Errorf("override policy %s must not have empty properties", policy.Name)
   321  		}
   322  		overrideSpec := &v1alpha1.OverridePolicySpec{}
   323  		if err := utils.StrictUnmarshal(policy.Properties.Raw, overrideSpec); err != nil {
   324  			return nil, errors.Wrapf(err, "failed to parse override policy %s", policy.Name)
   325  		}
   326  		overrideComps, err := envbinding.PatchComponents(app.Spec.Components, overrideSpec.Components, overrideSpec.Selector)
   327  		if err != nil {
   328  			return nil, errors.Wrapf(err, "failed to apply override policy %s", policy.Name)
   329  		}
   330  		app.Spec.Components = overrideComps
   331  	}
   332  
   333  	return app, nil
   334  }