Skip to content

Commit

Permalink
Add KRM func suppport for PackageVariants (#3925)
Browse files Browse the repository at this point in the history
* add deepcopy for kptfile funcs and selectors

* add KRM func suppport for PackageVariants

* add handling for removed mutators/validators

* remove comment

* allow for mutator/validator name to be empty

* switch to getFileKubeObject utility func

* edit validators/mutators in single loop

* document functionality of ensureKRMFunctions

* fix formatting after rebase

* add deleting existin pv mutators/validators and pruning of pipeline field

* Fix failing PVS unit test

---------

Co-authored-by: John Belamaric <jbelamaric@google.com>
  • Loading branch information
SimonTheLeg and johnbelamaric committed May 16, 2023
1 parent c1ed58d commit f40fd3d
Show file tree
Hide file tree
Showing 7 changed files with 667 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/design-docs/08-package-variant.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ Then the resulting Kptfile will have these two entries prepended to its
- image: gcr.io/kpt-fn/set-labels:v0.1
configMap:
app: foo
name: PackageVariant.my-pv.1
name: PackageVariant.my-pv..1
```

During subsequent reconciliations, this allows the controller to identify the
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/kptfile/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"sigs.k8s.io/kustomize/kyaml/yaml"
)

//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 object object:headerFile="../../../../porch/scripts/boilerplate.go.txt"

const (
KptFileName = "Kptfile"

Expand Down Expand Up @@ -280,6 +282,7 @@ func (p *Pipeline) IsEmpty() bool {
}

// Function specifies a KRM function.
// +kubebuilder:object:generate=true
type Function struct {
// `Image` specifies the function container image.
// It can either be fully qualified, e.g.:
Expand Down Expand Up @@ -325,6 +328,7 @@ type Function struct {

// Selector specifies the selection criteria
// please update IsEmpty method if more properties are added
// +kubebuilder:object:generate=true
type Selector struct {
// APIVersion of the target resources
APIVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"`
Expand Down
87 changes: 87 additions & 0 deletions pkg/api/kptfile/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package v1alpha1

import (
kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -64,6 +65,7 @@ type PackageVariantSpec struct {
Annotations map[string]string `json:"annotations,omitempty"`

PackageContext *PackageContext `json:"packageContext,omitempty"`
Pipeline *kptfilev1.Pipeline `json:"pipeline,omitempty"`
Injectors []InjectionSelector `json:"injectors,omitempty"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
api "github.com/GoogleContainerTools/kpt/porch/controllers/packagevariants/api/v1alpha1"

"github.com/GoogleContainerTools/kpt-functions-sdk/go/fn"
kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"

"golang.org/x/mod/semver"
"k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -773,6 +774,10 @@ func (r *PackageVariantReconciler) calculateDraftResources(ctx context.Context,
return nil, false, err
}

if err := ensureKRMFunctions(pv, &prr); err != nil {
return nil, false, err
}

if err := ensureConfigInjection(ctx, r.Client, pv, &prr); err != nil {
return nil, false, err
}
Expand Down Expand Up @@ -852,6 +857,130 @@ func getFileKubeObject(prr *porchapi.PackageRevisionResources, file, kind, name
return ko, nil
}

// ensureKRMFunctions adds mutators and validators specified in the PackageVariant to the kptfile inside the PackageRevisionResources.
// It generates a unique name that identifies the func (see func generatePVFuncname) and moves it to the top of the mutator sequence.
// It does not preserve yaml indent-style.
func ensureKRMFunctions(pv *api.PackageVariant,
prr *porchapi.PackageRevisionResources) error {

// parse kptfile
kptfile, err := getFileKubeObject(prr, kptfilev1.KptFileName, "", "")
if err != nil {
return err
}
pipeline := kptfile.UpsertMap("pipeline")

fieldlist := map[string][]kptfilev1.Function{
"validators": nil,
"mutators": nil,
}
// retrieve fields if pipeline is not nil, to avoid nilpointer exception
if pv.Spec.Pipeline != nil {
fieldlist["validators"] = pv.Spec.Pipeline.Validators
fieldlist["mutators"] = pv.Spec.Pipeline.Mutators
}

for fieldname, field := range fieldlist {
var newFieldVal = fn.SliceSubObjects{}

existingFields, ok, err := pipeline.NestedSlice(fieldname)
if err != nil {
return err
}
if !ok || existingFields == nil {
existingFields = fn.SliceSubObjects{}
}

for _, existingField := range existingFields {
ok, err := isPackageVariantFunc(existingField, pv.ObjectMeta.Name)
if err != nil {
return err
}
if !ok {
newFieldVal = append(newFieldVal, existingField)
}
}

var newPVFieldVal = fn.SliceSubObjects{}
for i, newFields := range field {
newFieldVal := newFields.DeepCopy()
newFieldVal.Name = generatePVFuncName(newFields.Name, pv.ObjectMeta.Name, i)
f, err := fn.NewFromTypedObject(newFieldVal)
if err != nil {
return err
}
newPVFieldVal = append(newPVFieldVal, &f.SubObject)
}

newFieldVal = append(newPVFieldVal, newFieldVal...)

// if there are new mutators/validators, set them. Otherwise delete the field. This avoids ugly dangling `mutators: []` fields in the final kptfile
if len(newFieldVal) > 0 {
if err := pipeline.SetSlice(newFieldVal, fieldname); err != nil {
return err
}
} else {
if _, err := pipeline.RemoveNestedField(fieldname); err != nil {
return err
}
}
}

// if there are no mutators and no validators, remove the dangling pipeline field
if pipeline.GetMap("mutators") == nil && pipeline.GetMap("validators") == nil {
if _, err := kptfile.RemoveNestedField("pipeline"); err != nil {
return err
}
}

// update kptfile
prr.Spec.Resources[kptfilev1.KptFileName] = kptfile.String()

return nil
}

const PackageVariantFuncPrefix = "PackageVariant"

// isPackageVariantFunc returns true if a function has been created via a PackageVariant.
// It uses the name of the func to determine its origin and compares it with the supplied pvName.
func isPackageVariantFunc(fn *fn.SubObject, pvName string) (bool, error) {
origname, ok, err := fn.NestedString("name")
if err != nil {
return false, fmt.Errorf("could not retrieve field name: %w", err)
}
if !ok {
return false, fmt.Errorf("could not find field name in supplied func")
}

name := strings.Split(origname, ".")

// if more or less than 3 dots have been used, return false
if len(name) != 4 {
return false, nil
}

// if PackageVariantFuncPrefix has not been used, return false
if name[0] != PackageVariantFuncPrefix {
return false, nil
}

// if pv-names don't match, return false
if name[1] != pvName {
return false, nil
}

// if the last segment is not an integer, return false
if _, err := strconv.Atoi(name[3]); err != nil {
return false, nil
}

return true, nil
}

func generatePVFuncName(funcName, pvName string, pos int) string {
return fmt.Sprintf("%s.%s.%s.%d", PackageVariantFuncPrefix, pvName, funcName, pos)
}

func hashFromPackageRevisionResources(prr *porchapi.PackageRevisionResources) (string, error) {
b, err := yaml.Marshal(prr.Spec.Resources)
if err != nil {
Expand Down
Loading

0 comments on commit f40fd3d

Please sign in to comment.