From 9f6ba300e9ee2db00d5a290e5f157b5ba144b210 Mon Sep 17 00:00:00 2001 From: Dmitry Volodin Date: Mon, 20 Jan 2025 20:02:14 +0300 Subject: [PATCH] :seedling: Add util which is able to merge golang source code fragments based custom rules --- go.mod | 1 + go.sum | 2 + pkg/plugin/util/merge/merge.go | 150 ++++++++++++++++++ pkg/plugin/util/merge/merge_test.go | 31 ++++ pkg/plugin/util/merge/suite_test.go | 29 ++++ pkg/plugin/util/merge/testdata/merge.go.file | 126 +++++++++++++++ pkg/plugin/util/merge/testdata/source.go.file | 69 ++++++++ 7 files changed, 408 insertions(+) create mode 100644 pkg/plugin/util/merge/merge.go create mode 100644 pkg/plugin/util/merge/merge_test.go create mode 100644 pkg/plugin/util/merge/suite_test.go create mode 100644 pkg/plugin/util/merge/testdata/merge.go.file create mode 100644 pkg/plugin/util/merge/testdata/source.go.file diff --git a/go.mod b/go.mod index 055f95b2fa5..292e5a35a53 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( ) require ( + github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index 436de2dbbc4..cd3229a9f19 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/pkg/plugin/util/merge/merge.go b/pkg/plugin/util/merge/merge.go new file mode 100644 index 00000000000..ca628444321 --- /dev/null +++ b/pkg/plugin/util/merge/merge.go @@ -0,0 +1,150 @@ +package merge + +import ( + "go/ast" + "go/parser" + "go/token" + "path" + "reflect" + "strconv" + "strings" + + "github.com/dave/dst" + "github.com/dave/dst/decorator" + "golang.org/x/tools/go/ast/astutil" +) + +func InsertCode(filename, code string) error { + fset := token.NewFileSet() + + sourceAst, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + return err + } + mergingAst, err := parser.ParseFile(fset, "", code, parser.ParseComments) + if err != nil { + return err + } + + for _, mImp := range mergingAst.Imports { + merging := true + for _, sImp := range sourceAst.Imports { + if importEqual(sImp, mImp) { + merging = false + break + } + } + if merging { + astutil.AddNamedImport(fset, sourceAst, defaultImportAlias(mImp), mustUnquoteImport(mImp)) + } + } + + sourceDst, err := decorator.DecorateFile(fset, sourceAst) + if err != nil { + return err + } + mergingDst, err := decorator.DecorateFile(fset, mergingAst) + if err != nil { + return err + } + + for _, mDecl := range mergingDst.Decls { + merging := true + for _, sDecl := range sourceDst.Decls { + if declEqual(sDecl, mDecl) { + merging = false + break + } + } + if merging { + sourceDst.Decls = append(sourceDst.Decls, mDecl) + } + } + + if err = decorator.Print(sourceDst); err != nil { + return err + } + + // dst.Fprint(os.Stdout, sourceDst, dst.NotNilFilter) + + return nil +} + +func declEqual(src, trg dst.Decl) bool { + if reflect.TypeOf(src) != reflect.TypeOf(trg) { + return false + } + switch srcType := src.(type) { + case *dst.GenDecl: + trgType := trg.(*dst.GenDecl) + if srcType.Tok != trgType.Tok { + return false + } + switch srcType.Tok { + case token.IMPORT: + return true + case token.TYPE: + if skipSpecs(srcType.Specs, trgType.Specs) { + return false + } + return srcType.Specs[0].(*dst.TypeSpec).Name.Name == trgType.Specs[0].(*dst.TypeSpec).Name.Name + case token.VAR: + if skipSpecs(srcType.Specs, trgType.Specs) { + return false + } + srcVarName := srcType.Specs[0].(*dst.ValueSpec).Names[0].Name + tgtVarName := trgType.Specs[0].(*dst.ValueSpec).Names[0].Name + if srcVarName == "_" && tgtVarName == "_" { + srcVarType := srcType.Specs[0].(*dst.ValueSpec).Type.(*dst.SelectorExpr) + tgtVarType := trgType.Specs[0].(*dst.ValueSpec).Type.(*dst.SelectorExpr) + + return srcVarType.Sel.Name == tgtVarType.Sel.Name + } + + return srcVarName == tgtVarName + default: + return true + } + case *dst.FuncDecl: + if srcType.Name == nil || trg.(*dst.FuncDecl).Name == nil { + return false + } + + return srcType.Name.Name == trg.(*dst.FuncDecl).Name.Name + } + + return false +} + +func skipSpecs(src, tgt []dst.Spec) bool { + return len(src) == 0 || len(tgt) == 0 || len(src) != len(tgt) +} + +func importEqual(src, trg *ast.ImportSpec) bool { + return src.Path.Value == trg.Path.Value && importAlias(src) == importAlias(trg) +} + +func defaultImportAlias(spec *ast.ImportSpec) string { + if spec.Name == nil { + return "" + } + + return spec.Name.Name +} + +func importAlias(spec *ast.ImportSpec) string { + if spec.Name == nil && spec.Path != nil { + return strings.ReplaceAll(path.Base(mustUnquoteImport(spec)), "-", "") + } + + return spec.Name.Name +} + +func mustUnquoteImport(importSpec *ast.ImportSpec) string { + res, err := strconv.Unquote(importSpec.Path.Value) + if err != nil { + panic(err) + } + + return res +} diff --git a/pkg/plugin/util/merge/merge_test.go b/pkg/plugin/util/merge/merge_test.go new file mode 100644 index 00000000000..81ce2e83011 --- /dev/null +++ b/pkg/plugin/util/merge/merge_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2022 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package merge + +import ( + _ "embed" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +//go:embed testdata/merge.go.file +var mergeCode string + +var _ = Describe("MergeUtil", func() { + It("returns an error if the command fails", func() { + err := InsertCode("./testdata/source.go.file", mergeCode) + Expect(err).To(Succeed()) + }) +}) diff --git a/pkg/plugin/util/merge/suite_test.go b/pkg/plugin/util/merge/suite_test.go new file mode 100644 index 00000000000..54c9c448d7e --- /dev/null +++ b/pkg/plugin/util/merge/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package merge + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestStage(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Merge Utils Suite") +} diff --git a/pkg/plugin/util/merge/testdata/merge.go.file b/pkg/plugin/util/merge/testdata/merge.go.file new file mode 100644 index 00000000000..765702df3a4 --- /dev/null +++ b/pkg/plugin/util/merge/testdata/merge.go.file @@ -0,0 +1,126 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + batchv1beta1 "my.domain/guestbook/api/v1beta1" +) + +// nolint:unused +// log is for logging in this package. +var cronjoblog = logf.Log.WithName("cronjob-resource") + +// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv1beta1.CronJob{}). + WithValidator(&CronJobCustomValidator{}). + WithDefaulter(&CronJobCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-batch-my-domain-v1beta1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.my.domain,resources=cronjobs,verbs=create;update,versions=v1beta1,name=mcronjob-v1beta1.kb.io,admissionReviewVersions=v1 + +// CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind CronJob when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type CronJobCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. +func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + cronjob, ok := obj.(*batchv1beta1.CronJob) + + if !ok { + return fmt.Errorf("expected an CronJob object but got %T", obj) + } + cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-batch-my-domain-v1beta1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.my.domain,resources=cronjobs,verbs=create;update,versions=v1beta1,name=vcronjob-v1beta1.kb.io,admissionReviewVersions=v1 + +// CronJobCustomValidator struct is responsible for validating the CronJob resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type CronJobCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &CronJobCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. +func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + cronjob, ok := obj.(*batchv1beta1.CronJob) + if !ok { + return nil, fmt.Errorf("expected a CronJob object but got %T", obj) + } + cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName()) + + // TODO(user): fill in your validation logic upon object creation. + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. +func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + cronjob, ok := newObj.(*batchv1beta1.CronJob) + if !ok { + return nil, fmt.Errorf("expected a CronJob object for the newObj but got %T", newObj) + } + cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName()) + + // TODO(user): fill in your validation logic upon object update. + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. +func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + cronjob, ok := obj.(*batchv1beta1.CronJob) + if !ok { + return nil, fmt.Errorf("expected a CronJob object but got %T", obj) + } + cronjoblog.Info("Validation for CronJob upon deletion", "name", cronjob.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/pkg/plugin/util/merge/testdata/source.go.file b/pkg/plugin/util/merge/testdata/source.go.file new file mode 100644 index 00000000000..e430cb71f90 --- /dev/null +++ b/pkg/plugin/util/merge/testdata/source.go.file @@ -0,0 +1,69 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + batchv1beta1 "my.domain/guestbook/api/v1beta1" +) + +// nolint:unused +// log is for logging in this package. +var cronjoblog = logf.Log.WithName("cronjob-resource") + +// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. +func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&batchv1beta1.CronJob{}). + WithDefaulter(&CronJobCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-batch-my-domain-v1beta1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.my.domain,resources=cronjobs,verbs=create;update,versions=v1beta1,name=mcronjob-v1beta1.kb.io,admissionReviewVersions=v1 + +// CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind CronJob when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type CronJobCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. +func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + cronjob, ok := obj.(*batchv1beta1.CronJob) + + if !ok { + return fmt.Errorf("expected an CronJob object but got %T", obj) + } + cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +}