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 }