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

Generate foreach 2 #7

Closed
wants to merge 14 commits into from
97 changes: 79 additions & 18 deletions api/kyverno/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,9 +749,6 @@ type Generation struct {
// +optional
GenerateExisting *bool `json:"generateExisting,omitempty" yaml:"generateExisting,omitempty"`

// ResourceSpec contains information to select the resource.
ResourceSpec `json:",omitempty" yaml:",omitempty"`

// Synchronize controls if generated resources should be kept in-sync with their source resource.
// If Synchronize is set to "true" changes to generated resources will be overwritten with resource
// data from Data or the resource specified in the Clone declaration.
Expand All @@ -766,6 +763,19 @@ type Generation struct {
// +optional
OrphanDownstreamOnPolicyDelete bool `json:"orphanDownstreamOnPolicyDelete,omitempty" yaml:"orphanDownstreamOnPolicyDelete,omitempty"`

// +optional
GeneratePatterns `json:",omitempty" yaml:",omitempty"`

// ForEach applies generate rules to a list of sub-elements by creating a context for each entry in the list and looping over it to apply the specified logic.
// +optional
ForEachGeneration []ForEachGeneration `json:"foreach,omitempty" yaml:"foreach,omitempty"`
}

type GeneratePatterns struct {
// ResourceSpec contains information to select the resource.
// +kubebuilder:validation:Optional
ResourceSpec `json:",omitempty" yaml:",omitempty"`

// Data provides the resource declaration used to populate each generated resource.
// At most one of Data or Clone must be specified. If neither are provided, the generated
// resource will be created with default data only.
Expand All @@ -783,6 +793,25 @@ type Generation struct {
CloneList CloneList `json:"cloneList,omitempty" yaml:"cloneList,omitempty"`
}

type ForEachGeneration struct {
// List specifies a JMESPath expression that results in one or more elements
// to which the validation logic is applied.
List string `json:"list,omitempty" yaml:"list,omitempty"`

// Context defines variables and data sources that can be used during rule execution.
// +optional
Context []ContextEntry `json:"context,omitempty" yaml:"context,omitempty"`

// AnyAllConditions are used to determine if a policy rule should be applied by evaluating a
// set of conditions. The declaration can contain nested `any` or `all` statements.
// See: https://kyverno.io/docs/writing-policies/preconditions/
// +kubebuilder:validation:XPreserveUnknownFields
// +optional
AnyAllConditions *AnyAllConditions `json:"preconditions,omitempty" yaml:"preconditions,omitempty"`

GeneratePatterns `json:",omitempty" yaml:",omitempty"`
}

type CloneList struct {
// Namespace specifies source resource namespace.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Expand All @@ -799,28 +828,58 @@ type CloneList struct {
func (g *Generation) Validate(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.Set[string]) (errs field.ErrorList) {
if namespaced {
if err := g.validateNamespacedTargetsScope(clusterResources, policyNamespace); err != nil {
errs = append(errs, field.Forbidden(path.Child("generate").Child("namespace"), fmt.Sprintf("target resource scope mismatched: %v ", err)))
errs = append(errs, field.Forbidden(path.Child("namespace"), fmt.Sprintf("target resource scope mismatched: %v ", err)))
}
}

generateType, _, _ := g.GetTypeAndSyncAndOrphanDownstream()
if generateType == Data {
return errs
}

count := 0
if g.GetData() != nil {
count++
}
if g.Clone != (CloneFrom{}) {
count++
}
if g.CloneList.Kinds != nil {
count++
}
if g.ForEachGeneration != nil {
count++
}
if count > 1 {
errs = append(errs, field.Forbidden(path, "only one of data or clone can be specified"))
return errs
}

if g.ForEachGeneration != nil {
for i, foreach := range g.ForEachGeneration {
err := foreach.GeneratePatterns.Validate(path.Child("foreach").Index(i), namespaced, policyNamespace, clusterResources)
errs = append(errs, err...)
}
return errs
} else {
return g.GeneratePatterns.Validate(path, namespaced, policyNamespace, clusterResources)
}
}

func (g *GeneratePatterns) Validate(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.Set[string]) (errs field.ErrorList) {
if g.GetKind() != "" {
if !clusterResources.Has(g.GetAPIVersion() + "/" + g.GetKind()) {
if g.GetNamespace() == "" {
errs = append(errs, field.Forbidden(path.Child("generate").Child("namespace"), "target namespace must be set for a namespaced resource"))
errs = append(errs, field.Forbidden(path.Child("namespace"), "target namespace must be set for a namespaced resource"))
}
} else {
if g.GetNamespace() != "" {
errs = append(errs, field.Forbidden(path.Child("generate").Child("namespace"), "target namespace must not be set for a cluster-wide resource"))
errs = append(errs, field.Forbidden(path.Child("namespace"), "target namespace must not be set for a cluster-wide resource"))
}
}
}

generateType, _, _ := g.GetTypeAndSyncAndOrphanDownstream()
if generateType == Data {
return errs
}

newGeneration := Generation{
newGeneration := GeneratePatterns{
ResourceSpec: ResourceSpec{
Kind: g.ResourceSpec.GetKind(),
APIVersion: g.ResourceSpec.GetAPIVersion(),
Expand All @@ -830,23 +889,25 @@ func (g *Generation) Validate(path *field.Path, namespaced bool, policyNamespace
}

if err := regex.ObjectHasVariables(newGeneration); err != nil {
errs = append(errs, field.Forbidden(path.Child("generate").Child("clone/cloneList"), "Generation Rule Clone/CloneList should not have variables"))
errs = append(errs, field.Forbidden(path.Child("clone/cloneList"), "Generation Rule Clone/CloneList should not have variables"))
}

if len(g.CloneList.Kinds) == 0 {
if g.Kind == "" {
errs = append(errs, field.Forbidden(path.Child("generate").Child("kind"), "kind can not be empty"))
errs = append(errs, field.Forbidden(path.Child("kind"), "kind can not be empty"))
}
if g.Name == "" {
errs = append(errs, field.Forbidden(path.Child("generate").Child("name"), "name can not be empty"))
errs = append(errs, field.Forbidden(path.Child("name"), "name can not be empty"))
}
if g.APIVersion == "" {
errs = append(errs, field.Forbidden(path.Child("apiVersion"), "apiVersion can not be empty"))
}
}

errs = append(errs, g.ValidateCloneList(path.Child("generate"), namespaced, policyNamespace, clusterResources)...)
return errs
return append(errs, g.ValidateCloneList(path, namespaced, policyNamespace, clusterResources)...)
}

func (g *Generation) ValidateCloneList(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.Set[string]) (errs field.ErrorList) {
func (g *GeneratePatterns) ValidateCloneList(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.Set[string]) (errs field.ErrorList) {
if len(g.CloneList.Kinds) == 0 {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion api/kyverno/v1/rule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func (r *Rule) ValidateGenerate(path *field.Path, namespaced bool, policyNamespa
return nil
}

return r.Generation.Validate(path, namespaced, policyNamespace, clusterResources)
return r.Generation.Validate(path.Child("generate"), namespaced, policyNamespace, clusterResources)
}

// Validate implements programmatic validation
Expand Down
Loading
Loading