1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-05 07:26:55 +00:00

feat: allow cloning multiple resource from a namespace (#4384)

This commit is contained in:
Prateek Pandey 2022-09-08 10:17:09 +05:30 committed by GitHub
parent 560cec329e
commit 1cacd0173d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 670 additions and 166 deletions

View file

@ -492,6 +492,18 @@ type Generation struct {
// resource will be created with default data only.
// +optional
Clone CloneFrom `json:"clone,omitempty" yaml:"clone,omitempty"`
// CloneList specifies the list of source resource used to populate each generated resource.
// +optional
CloneList CloneList `json:"cloneList,omitempty" yaml:"cloneList,omitempty"`
}
type CloneList struct {
// Namespace specifies source resource namespace.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// Kinds is a list of resource kinds.
Kinds []string `json:"kinds,omitempty" yaml:"kinds,omitempty"`
}
func (g *Generation) GetData() apiextensions.JSON {

View file

@ -251,6 +251,26 @@ func (in *CloneFrom) DeepCopy() *CloneFrom {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CloneList) DeepCopyInto(out *CloneList) {
*out = *in
if in.Kinds != nil {
in, out := &in.Kinds, &out.Kinds
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloneList.
func (in *CloneList) DeepCopy() *CloneList {
if in == nil {
return nil
}
out := new(CloneList)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterPolicy) DeepCopyInto(out *ClusterPolicy) {
*out = *in
@ -622,6 +642,7 @@ func (in *Generation) DeepCopyInto(out *Generation) {
(*in).DeepCopyInto(*out)
}
out.Clone = in.Clone
in.CloneList.DeepCopyInto(&out.CloneList)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generation.

View file

@ -540,6 +540,18 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: 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.
x-kubernetes-preserve-unknown-fields: true
@ -2225,6 +2237,18 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: 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.
x-kubernetes-preserve-unknown-fields: true
@ -4729,6 +4753,18 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: 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.
x-kubernetes-preserve-unknown-fields: true
@ -6414,6 +6450,18 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: 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.
x-kubernetes-preserve-unknown-fields: true

View file

@ -840,6 +840,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -3566,6 +3579,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most

View file

@ -841,6 +841,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -3568,6 +3581,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most

View file

@ -857,6 +857,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -3583,6 +3596,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most
@ -7406,6 +7433,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -10133,6 +10173,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most

View file

@ -855,6 +855,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -3581,6 +3594,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most
@ -7400,6 +7427,19 @@ spec:
description: Namespace specifies source resource namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source resource
used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource namespace.
type: string
type: object
data:
description: Data provides the resource declaration used
to populate each generated resource. At most one of Data
@ -10127,6 +10167,20 @@ spec:
namespace.
type: string
type: object
cloneList:
description: CloneList specifies the list of source
resource used to populate each generated resource.
properties:
kinds:
description: Kinds is a list of resource kinds.
items:
type: string
type: array
namespace:
description: Namespace specifies source resource
namespace.
type: string
type: object
data:
description: Data provides the resource declaration
used to populate each generated resource. At most

View file

@ -1848,6 +1848,20 @@ At most one of Data or Clone can be specified. If neither are provided, the gene
resource will be created with default data only.</p>
</td>
</tr>
<tr>
<td>
<code>cloneList</code><br/>
<em>
<a href="#kyverno.io/v1.CloneList">
CloneList
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CloneList specifies the list of source resource used to populate each generated resource.</p>
</td>
</tr>
</tbody>
</table>
<hr />

View file

@ -28,6 +28,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/event"
kyvernoutils "github.com/kyverno/kyverno/pkg/utils"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -271,18 +272,17 @@ func (c *GenerateController) getPolicySpec(ur kyvernov1beta1.UpdateRequest) (kyv
return policy, err
}
return *policyObj, err
} else {
npolicyObj, err := c.npolicyLister.Policies(pNamespace).Get(pName)
if err != nil {
return policy, err
}
return kyvernov1.ClusterPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: pName,
},
Spec: npolicyObj.Spec,
}, nil
}
npolicyObj, err := c.npolicyLister.Policies(pNamespace).Get(pName)
if err != nil {
return policy, err
}
return kyvernov1.ClusterPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: pName,
},
Spec: npolicyObj.Spec,
}, nil
}
func updateStatus(statusControl common.StatusControlInterface, ur kyvernov1beta1.UpdateRequest, err error, genResources []kyvernov1.ResourceSpec, precreatedResource bool) error {
@ -326,7 +326,7 @@ func (c *GenerateController) ApplyGeneratePolicy(log logr.Logger, policyContext
startTime := time.Now()
processExisting = false
var genResource kyvernov1.ResourceSpec
var genResource []kyvernov1.ResourceSpec
if len(rule.MatchResources.Kinds) > 0 {
if len(rule.MatchResources.Annotations) == 0 && rule.MatchResources.Selector == nil {
@ -359,7 +359,7 @@ func (c *GenerateController) ApplyGeneratePolicy(log logr.Logger, policyContext
return nil, processExisting, err
}
ruleNameToProcessingTime[rule.Name] = time.Since(startTime)
genResources = append(genResources, genResource)
genResources = append(genResources, genResource...)
}
if policy.GetSpec().IsGenerateExistingOnPolicyUpdate() {
@ -392,23 +392,190 @@ func getResourceInfo(object map[string]interface{}) (kind, name, namespace, apiv
return
}
func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, resource unstructured.Unstructured, ctx context.EvalInterface, policy kyvernov1.PolicyInterface, ur kyvernov1beta1.UpdateRequest) (kyvernov1.ResourceSpec, error) {
var rdata map[string]interface{}
func getResourceInfoForDataAndClone(rule kyvernov1.Rule) (kind, name, namespace, apiversion string, err error) {
if len(rule.Generation.CloneList.Kinds) == 0 {
if kind = rule.Generation.Kind; kind == "" {
return "", "", "", "", fmt.Errorf("%s", "kind can not be empty")
}
if name = rule.Generation.Name; name == "" {
return "", "", "", "", fmt.Errorf("%s", "name can not be empty")
}
}
namespace = rule.Generation.Namespace
apiversion = rule.Generation.APIVersion
return
}
func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, resource unstructured.Unstructured, ctx context.EvalInterface, policy kyvernov1.PolicyInterface, ur kyvernov1beta1.UpdateRequest) ([]kyvernov1.ResourceSpec, error) {
rdatas := []GenerateResponse{}
var cresp, dresp map[string]interface{}
var err error
var mode ResourceMode
var noGenResource kyvernov1.ResourceSpec
genUnst, err := GetUnstrRule(rule.Generation.DeepCopy())
if err != nil {
return noGenResource, err
}
var newGenResources []kyvernov1.ResourceSpec
genKind, genName, genNamespace, genAPIVersion, err := getResourceInfo(genUnst.Object)
genKind, genName, genNamespace, genAPIVersion, err := getResourceInfoForDataAndClone(rule)
if err != nil {
return noGenResource, err
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
logger := log.WithValues("genKind", genKind, "genAPIVersion", genAPIVersion, "genNamespace", genNamespace, "genName", genName)
if rule.Generation.Clone.Name != "" {
cresp, mode, err = manageClone(logger, genAPIVersion, genKind, genNamespace, genName, policy.GetName(), rule.Generation, client)
rdatas = append(rdatas, GenerateResponse{
Data: cresp,
Action: mode,
GenName: genName,
GenKind: genKind,
GenNamespace: genNamespace,
GenAPIVersion: genAPIVersion,
Error: err,
})
} else if len(rule.Generation.CloneList.Kinds) != 0 {
rdatas = manageCloneList(logger, genNamespace, policy.GetName(), rule.Generation, client)
} else {
dresp, mode, err = manageData(logger, genAPIVersion, genKind, genNamespace, genName, rule.Generation.RawData, client)
rdatas = append(rdatas, GenerateResponse{
Data: dresp,
Action: mode,
GenName: genName,
GenKind: genKind,
GenNamespace: genNamespace,
GenAPIVersion: genAPIVersion,
Error: err,
})
}
for _, rdata := range rdatas {
if rdata.Error != nil {
logger.Error(err, "failed to generate resource", "mode", rdata.Action)
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
logger.V(3).Info("applying generate rule", "mode", rdata.Action)
// skip processing the response in case of skip action
if rdata.Action == Skip {
continue
}
if rdata.Data == nil && rdata.Action == Update {
logger.V(4).Info("no changes required for generate target resource")
newGenResources = append(newGenResources, noGenResource)
return newGenResources, nil
}
// build the resource template
newResource := &unstructured.Unstructured{}
newResource.SetUnstructuredContent(rdata.Data)
newResource.SetName(rdata.GenName)
newResource.SetNamespace(rdata.GenNamespace)
if newResource.GetKind() == "" {
newResource.SetKind(rdata.GenKind)
}
newResource.SetAPIVersion(rdata.GenAPIVersion)
// manage labels
// - app.kubernetes.io/managed-by: kyverno
// "kyverno.io/generated-by-kind": kind (trigger resource)
// "kyverno.io/generated-by-namespace": namespace (trigger resource)
// "kyverno.io/generated-by-name": name (trigger resource)
common.ManageLabels(newResource, resource)
// Add Synchronize label
label := newResource.GetLabels()
// Add background gen-rule label if generate rule applied on existing resource
if policy.GetSpec().IsGenerateExistingOnPolicyUpdate() {
label["kyverno.io/background-gen-rule"] = rule.Name
}
label["policy.kyverno.io/policy-name"] = policy.GetName()
label["policy.kyverno.io/gr-name"] = ur.Name
if rdata.Action == Create {
if rule.Generation.Synchronize {
label["policy.kyverno.io/synchronize"] = "enable"
} else {
label["policy.kyverno.io/synchronize"] = "disable"
}
// Reset resource version
newResource.SetResourceVersion("")
newResource.SetLabels(label)
// Create the resource
_, err = client.CreateResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, newResource, false)
if err != nil {
if !apierrors.IsAlreadyExists(err) {
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
}
logger.V(2).Info("created generate target resource")
newGenResources = append(newGenResources, newGenResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, rdata.GenName))
} else if rdata.Action == Update {
generatedObj, err := client.GetResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, rdata.GenName)
if err != nil {
logger.Error(err, fmt.Sprintf("generated resource not found name:%v namespace:%v kind:%v", genName, genNamespace, genKind))
logger.V(2).Info(fmt.Sprintf("creating generate resource name:name:%v namespace:%v kind:%v", genName, genNamespace, genKind))
_, err = client.CreateResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, newResource, false)
if err != nil {
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
newGenResources = append(newGenResources, newGenResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, rdata.GenName))
} else {
// if synchronize is true - update the label and generated resource with generate policy data
if rule.Generation.Synchronize {
logger.V(4).Info("updating existing resource")
label["policy.kyverno.io/synchronize"] = "enable"
newResource.SetLabels(label)
if rdata.GenAPIVersion == "" {
generatedResourceAPIVersion := generatedObj.GetAPIVersion()
newResource.SetAPIVersion(generatedResourceAPIVersion)
}
if rdata.GenNamespace == "" {
newResource.SetNamespace("default")
}
if _, err := ValidateResourceWithPattern(logger, generatedObj.Object, newResource.Object); err != nil {
_, err = client.UpdateResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, newResource, false)
if err != nil {
logger.Error(err, "failed to update resource")
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
}
} else {
currentGeneratedResourcelabel := generatedObj.GetLabels()
currentSynclabel := currentGeneratedResourcelabel["policy.kyverno.io/synchronize"]
// update only if the labels mismatches
if (!rule.Generation.Synchronize && currentSynclabel == "enable") ||
(rule.Generation.Synchronize && currentSynclabel == "disable") {
logger.V(4).Info("updating label in existing resource")
currentGeneratedResourcelabel["policy.kyverno.io/synchronize"] = "disable"
generatedObj.SetLabels(currentGeneratedResourcelabel)
_, err = client.UpdateResource(rdata.GenAPIVersion, rdata.GenKind, rdata.GenNamespace, generatedObj, false)
if err != nil {
logger.Error(err, "failed to update label in existing resource")
newGenResources = append(newGenResources, noGenResource)
return newGenResources, err
}
}
}
}
logger.V(3).Info("updated generate target resource")
}
}
return newGenResources, nil
}
func newGenResource(genAPIVersion, genKind, genNamespace, genName string) kyvernov1.ResourceSpec {
// Resource to be generated
newGenResource := kyvernov1.ResourceSpec{
APIVersion: genAPIVersion,
@ -416,139 +583,19 @@ func applyRule(log logr.Logger, client dclient.Interface, rule kyvernov1.Rule, r
Namespace: genNamespace,
Name: genName,
}
genData, _, err := unstructured.NestedMap(genUnst.Object, "data")
if err != nil {
return noGenResource, fmt.Errorf("failed to read `data`: %v", err.Error())
}
genClone, _, err := unstructured.NestedMap(genUnst.Object, "clone")
if err != nil {
return noGenResource, fmt.Errorf("failed to read `clone`: %v", err.Error())
}
if len(genClone) != 0 {
rdata, mode, err = manageClone(logger, genAPIVersion, genKind, genNamespace, genName, policy.GetName(), genClone, client)
} else {
rdata, mode, err = manageData(logger, genAPIVersion, genKind, genNamespace, genName, genData, client)
}
if err != nil {
logger.Error(err, "failed to generate resource", "mode", mode)
return newGenResource, err
}
logger.V(3).Info("applying generate rule", "mode", mode)
if rdata == nil && mode == Update {
logger.V(4).Info("no changes required for generate target resource")
return newGenResource, nil
}
// build the resource template
newResource := &unstructured.Unstructured{}
newResource.SetUnstructuredContent(rdata)
newResource.SetName(genName)
newResource.SetNamespace(genNamespace)
if newResource.GetKind() == "" {
newResource.SetKind(genKind)
}
newResource.SetAPIVersion(genAPIVersion)
// manage labels
// - app.kubernetes.io/managed-by: kyverno
// "kyverno.io/generated-by-kind": kind (trigger resource)
// "kyverno.io/generated-by-namespace": namespace (trigger resource)
// "kyverno.io/generated-by-name": name (trigger resource)
common.ManageLabels(newResource, resource)
// Add Synchronize label
label := newResource.GetLabels()
// Add background gen-rule label if generate rule applied on existing resource
if policy.GetSpec().IsGenerateExistingOnPolicyUpdate() {
label["kyverno.io/background-gen-rule"] = rule.Name
}
label["policy.kyverno.io/policy-name"] = policy.GetName()
label["policy.kyverno.io/gr-name"] = ur.Name
if mode == Create {
if rule.Generation.Synchronize {
label["policy.kyverno.io/synchronize"] = "enable"
} else {
label["policy.kyverno.io/synchronize"] = "disable"
}
// Reset resource version
newResource.SetResourceVersion("")
newResource.SetLabels(label)
// Create the resource
_, err = client.CreateResource(genAPIVersion, genKind, genNamespace, newResource, false)
if err != nil {
return noGenResource, err
}
logger.V(2).Info("created generate target resource")
} else if mode == Update {
generatedObj, err := client.GetResource(genAPIVersion, genKind, genNamespace, genName)
if err != nil {
logger.Error(err, fmt.Sprintf("generated resource not found name:%v namespace:%v kind:%v", genName, genNamespace, genKind))
logger.V(2).Info(fmt.Sprintf("creating generate resource name:name:%v namespace:%v kind:%v", genName, genNamespace, genKind))
_, err = client.CreateResource(genAPIVersion, genKind, genNamespace, newResource, false)
if err != nil {
return noGenResource, err
}
} else {
// if synchronize is true - update the label and generated resource with generate policy data
if rule.Generation.Synchronize {
logger.V(4).Info("updating existing resource")
label["policy.kyverno.io/synchronize"] = "enable"
newResource.SetLabels(label)
if genAPIVersion == "" {
generatedResourceAPIVersion := generatedObj.GetAPIVersion()
newResource.SetAPIVersion(generatedResourceAPIVersion)
}
if genNamespace == "" {
newResource.SetNamespace("default")
}
if _, err := ValidateResourceWithPattern(logger, generatedObj.Object, newResource.Object); err != nil {
_, err = client.UpdateResource(genAPIVersion, genKind, genNamespace, newResource, false)
if err != nil {
logger.Error(err, "failed to update resource")
return noGenResource, err
}
}
} else {
currentGeneratedResourcelabel := generatedObj.GetLabels()
currentSynclabel := currentGeneratedResourcelabel["policy.kyverno.io/synchronize"]
// update only if the labels mismatches
if (!rule.Generation.Synchronize && currentSynclabel == "enable") ||
(rule.Generation.Synchronize && currentSynclabel == "disable") {
logger.V(4).Info("updating label in existing resource")
currentGeneratedResourcelabel["policy.kyverno.io/synchronize"] = "disable"
generatedObj.SetLabels(currentGeneratedResourcelabel)
_, err = client.UpdateResource(genAPIVersion, genKind, genNamespace, generatedObj, false)
if err != nil {
logger.Error(err, "failed to update label in existing resource")
return noGenResource, err
}
}
}
}
logger.V(3).Info("updated generate target resource")
}
return newGenResource, nil
return newGenResource
}
func manageData(log logr.Logger, apiVersion, kind, namespace, name string, data map[string]interface{}, client dclient.Interface) (map[string]interface{}, ResourceMode, error) {
func manageData(log logr.Logger, apiVersion, kind, namespace, name string, data interface{}, client dclient.Interface) (map[string]interface{}, ResourceMode, error) {
resource, err := kyvernoutils.ToMap(data)
if err != nil {
return nil, Skip, err
}
obj, err := client.GetResource(apiVersion, kind, namespace, name)
if err != nil {
if apierrors.IsNotFound(err) {
return data, Create, nil
return resource, Create, nil
}
log.Error(err, "failed to get resource")
@ -562,20 +609,21 @@ func manageData(log logr.Logger, apiVersion, kind, namespace, name string, data
}
updateObj := &unstructured.Unstructured{}
updateObj.SetUnstructuredContent(data)
updateObj.SetUnstructuredContent(resource)
updateObj.SetResourceVersion(obj.GetResourceVersion())
return updateObj.UnstructuredContent(), Update, nil
}
func manageClone(log logr.Logger, apiVersion, kind, namespace, name, policy string, clone map[string]interface{}, client dclient.Interface) (map[string]interface{}, ResourceMode, error) {
rNamespace, _, err := unstructured.NestedString(clone, "namespace")
if err != nil {
return nil, Skip, fmt.Errorf("failed to find source namespace: %v", err)
func manageClone(log logr.Logger, apiVersion, kind, namespace, name, policy string, clone kyvernov1.Generation, client dclient.Interface) (map[string]interface{}, ResourceMode, error) {
// resource namespace can be nil in case of clusters scope resource
rNamespace := clone.Clone.Namespace
if rNamespace == "" {
log.V(4).Info("resource namespace %s , optional in case of cluster scope resource", rNamespace)
}
rName, _, err := unstructured.NestedString(clone, "name")
if err != nil {
return nil, Skip, fmt.Errorf("failed to find source name: %v", err)
rName := clone.Clone.Name
if rName == "" {
return nil, Skip, fmt.Errorf("failed to find source name")
}
if rNamespace == namespace && rName == name {
@ -611,6 +659,110 @@ func manageClone(log logr.Logger, apiVersion, kind, namespace, name, policy stri
return obj.UnstructuredContent(), Create, nil
}
func manageCloneList(log logr.Logger, namespace, policy string, clone kyvernov1.Generation, client dclient.Interface) []GenerateResponse {
var response []GenerateResponse
rNamespace := clone.CloneList.Namespace
if rNamespace == "" {
log.V(4).Info("resource namespace %s , optional in case of cluster scope resource", rNamespace)
}
kinds := clone.CloneList.Kinds
if len(kinds) == 0 {
response = append(response, GenerateResponse{
Data: nil,
Action: Skip,
Error: fmt.Errorf("failed to find kinds list"),
})
}
for _, kind := range kinds {
apiVersion, kind := kubeutils.GetKindFromGVK(kind)
resources, err := client.ListResource(apiVersion, kind, rNamespace, nil)
if err != nil {
response = append(response, GenerateResponse{
Data: nil,
Action: Skip,
Error: fmt.Errorf("failed to list resource %s %s/%s. %v", apiVersion, kind, rNamespace, err),
})
}
for _, rName := range resources.Items {
if rNamespace == namespace {
log.V(4).Info("skip resource self-clone")
response = append(response, GenerateResponse{
Data: nil,
Action: Skip,
Error: nil,
})
}
// check if the resource as reference in clone exists?
obj, err := client.GetResource(apiVersion, kind, rNamespace, rName.GetName())
if err != nil {
log.Error(err, "failed to get resoruce", apiVersion, "apiVersion", kind, "kind", rNamespace, "rNamespace", rName.GetName(), "name")
response = append(response, GenerateResponse{
Data: nil,
Action: Skip,
Error: fmt.Errorf("source resource %s %s/%s/%s not found. %v", apiVersion, kind, rNamespace, rName.GetName(), err),
})
return response
}
// remove ownerReferences when cloning resources to other namespace
if rNamespace != namespace && obj.GetOwnerReferences() != nil {
obj.SetOwnerReferences(nil)
}
// check if resource to be generated exists
newResource, err := client.GetResource(apiVersion, kind, namespace, rName.GetName())
if err == nil && newResource != nil {
obj.SetUID(newResource.GetUID())
obj.SetSelfLink(newResource.GetSelfLink())
obj.SetCreationTimestamp(newResource.GetCreationTimestamp())
obj.SetManagedFields(newResource.GetManagedFields())
obj.SetResourceVersion(newResource.GetResourceVersion())
if reflect.DeepEqual(obj, newResource) {
response = append(response, GenerateResponse{
Data: nil,
Action: Skip,
Error: nil,
})
} else {
response = append(response, GenerateResponse{
Data: obj.UnstructuredContent(),
Action: Update,
GenKind: kind,
GenName: rName.GetName(),
GenNamespace: namespace,
GenAPIVersion: apiVersion,
Error: nil,
})
}
}
// create the resource based on the reference clone
response = append(response, GenerateResponse{
Data: obj.UnstructuredContent(),
Action: Create,
GenKind: kind,
GenName: rName.GetName(),
GenNamespace: namespace,
GenAPIVersion: apiVersion,
Error: nil,
})
}
}
return response
}
type GenerateResponse struct {
Data map[string]interface{}
Action ResourceMode
GenKind, GenName, GenNamespace, GenAPIVersion string
Error error
}
// ResourceMode defines the mode for generated resource
type ResourceMode string

View file

@ -40,18 +40,24 @@ func (g *Generate) Validate() (string, error) {
return "", fmt.Errorf("only one of data or clone can be specified")
}
if rule.Clone != (kyvernov1.CloneFrom{}) && len(rule.CloneList.Kinds) != 0 {
return "", fmt.Errorf("only one of clone or cloneList can be specified")
}
kind, name, namespace := rule.Kind, rule.Name, rule.Namespace
if name == "" {
return "name", fmt.Errorf("name cannot be empty")
}
if kind == "" {
return "kind", fmt.Errorf("kind cannot be empty")
if len(rule.CloneList.Kinds) == 0 {
if name == "" {
return "name", fmt.Errorf("name cannot be empty")
}
if kind == "" {
return "kind", fmt.Errorf("kind cannot be empty")
}
}
// Can I generate resource
if !reflect.DeepEqual(rule.Clone, kyvernov1.CloneFrom{}) {
if path, err := g.validateClone(rule.Clone, kind); err != nil {
if path, err := g.validateClone(rule.Clone, rule.CloneList, kind); err != nil {
return fmt.Sprintf("clone.%s", path), err
}
}
@ -74,9 +80,11 @@ func (g *Generate) Validate() (string, error) {
return "", nil
}
func (g *Generate) validateClone(c kyvernov1.CloneFrom, kind string) (string, error) {
if c.Name == "" {
return "name", fmt.Errorf("name cannot be empty")
func (g *Generate) validateClone(c kyvernov1.CloneFrom, cl kyvernov1.CloneList, kind string) (string, error) {
if len(cl.Kinds) == 0 {
if c.Name == "" {
return "name", fmt.Errorf("name cannot be empty")
}
}
namespace := c.Namespace

View file

@ -31,6 +31,9 @@ var (
// ConfigMap GVR
cmGVR = e2e.GetGVR("", "v1", "configmaps")
// Secret GVR
secretGVR = e2e.GetGVR("", "v1", "secrets")
// NetworkPolicy GVR
npGVR = e2e.GetGVR("networking.k8s.io", "v1", "networkpolicies")
)
@ -278,3 +281,37 @@ var generatePolicyDeletionforCloneTests = []testCase{
},
},
}
var generatePolicyMultipleCloneTests = []testCase{
{
TestName: "test-multiple-clone-resources",
ClusterPolicy: clusterPolicy(genMultipleClonePolicyYaml),
SourceResources: resources(
configMap("default", cloneSourceResource),
secret("default", cloneSecretSourceResource),
),
TriggerResource: namespace(namespaceYaml),
ExpectedResources: expectations(
expectation(idConfigMap("test", "game-demo")),
expectation(idSecret("test", "secret-basic-auth")),
),
Steps: []testCaseStep{
stepExpectResource(cmGVR, "test", "game-demo"),
stepBy("verify generated resource data in configMap"),
stepExpectResource(cmGVR, "test", "game-demo", func(resource *unstructured.Unstructured) {
element, _, err := unstructured.NestedMap(resource.UnstructuredContent(), "data")
Expect(err).NotTo(HaveOccurred())
Expect(element["initial_lives"]).To(Equal("2"))
}),
stepBy("verify generated resource data in secret"),
stepExpectResource(secretGVR, "test", "secret-basic-auth"),
stepBy("deleted source -> generated resource not deleted"),
stepDeleteResource(cmGVR, "default", "game-demo"),
stepDeleteResource(secretGVR, "default", "secret-basic-auth"),
stepExpectResource(cmGVR, "test", "game-demo"),
stepExpectResource(secretGVR, "test", "secret-basic-auth"),
},
},
}

View file

@ -110,3 +110,7 @@ func Test_Source_Resource_Update_Replication(t *testing.T) {
func Test_Generate_Policy_Deletion_for_Clone(t *testing.T) {
runTestCases(t, generatePolicyDeletionforCloneTests...)
}
func Test_Generate_Multiple_Clone(t *testing.T) {
runTestCases(t, generatePolicyMultipleCloneTests...)
}

View file

@ -29,6 +29,7 @@ func resources(resources ...resource) []resource { return resources }
func role(ns string, raw []byte) resource { return namespaced(rGVR, ns, raw) }
func roleBinding(ns string, raw []byte) resource { return namespaced(rbGVR, ns, raw) }
func configMap(ns string, raw []byte) resource { return namespaced(cmGVR, ns, raw) }
func secret(ns string, raw []byte) resource { return namespaced(secretGVR, ns, raw) }
func clusterPolicy(raw []byte) resource { return clustered(clPolGVR, raw) }
func clusterRole(raw []byte) resource { return clustered(crGVR, raw) }
func clusterRoleBinding(raw []byte) resource { return clustered(crbGVR, raw) }
@ -47,6 +48,7 @@ func id(gvr schema.GroupVersionResource, ns string, name string) _id {
func idRole(ns, name string) _id { return id(rGVR, ns, name) }
func idRoleBinding(ns, name string) _id { return id(rbGVR, ns, name) }
func idConfigMap(ns, name string) _id { return id(cmGVR, ns, name) }
func idSecret(ns, name string) _id { return id(secretGVR, ns, name) }
func idNetworkPolicy(ns, name string) _id { return id(npGVR, ns, name) }
func idClusterRole(name string) _id { return id(crGVR, "", name) }
func idClusterRoleBinding(name string) _id { return id(crbGVR, "", name) }

View file

@ -377,6 +377,17 @@ data:
initial_lives: "2"
`)
var cloneSecretSourceResource = []byte(`
apiVersion: v1
kind: Secret
metadata:
name: secret-basic-auth
type: kubernetes.io/basic-auth
stringData:
username: admin
password: t0p-Secret
`)
var genCloneConfigMapPolicyYaml = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
@ -405,3 +416,36 @@ spec:
namespace: default
name: game-demo
`)
var genMultipleClonePolicyYaml = []byte(`
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sync-secret-with-multi-clone
spec:
generateExistingOnPolicyUpdate: true
rules:
- name: sync-secret
match:
any:
- resources:
kinds:
- Namespace
exclude:
any:
- resources:
namespaces:
- kube-system
- default
- kube-public
- kyverno
generate:
kind: Secret
namespace: "{{request.object.metadata.name}}"
synchronize : true
cloneList:
namespace: default
kinds:
- v1/Secret
- v1/ConfigMap
`)