Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KRM func suppport for PackageVariants #3925

Merged
merged 11 commits into from
May 16, 2023
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 @@ -513,7 +513,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
johnbelamaric marked this conversation as resolved.
Show resolved Hide resolved
```

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 @@ -772,6 +773,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 @@ -851,6 +856,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
}
}
}

johnbelamaric marked this conversation as resolved.
Show resolved Hide resolved
// 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
johnbelamaric marked this conversation as resolved.
Show resolved Hide resolved
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