1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

Merge branch '152_automate_testing' of github.com:nirmata/kyverno into 152_automate_testing

This commit is contained in:
shivdudhani 2019-06-19 15:13:11 -07:00
commit e1d4effd4e
34 changed files with 1629 additions and 531 deletions

39
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,39 @@
# Kyverno Community Code of Conduct v1.0
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering
an open and welcoming community, we pledge to respect all people who contribute
through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses,
without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers
commit themselves to fairly and consistently applying these principles to every aspect
of managing this project. Project maintainers who do not follow or enforce the Code of
Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior in Kubernetes may be reported by contacting the project maintainer(s).
This Code of Conduct is adapted from the the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md) and the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/

View file

@ -114,22 +114,28 @@ spec:
Additional examples are available in [examples](/examples).
## License
[Apache License 2.0](https://github.com/nirmata/kyverno/blob/master/LICENSE)
## Status
*Kyverno is under active development and not ready for production use. Key components and policy definitions are likely to change as we complete core features.*
## Alternatives
### Open Policy Agent
[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) is a general-purpose policy engine that can be used as a Kubernetes admission controller. It supports a large set of use cases. Policies are written using [Rego](https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies#what-is-rego) a custom query language.
### Polaris
[Polaris](https://github.com/reactiveops/polaris) validates configurations for best practices. It includes several checks across health, networking, security, etc. Checks can be assigned a severity. A dashboard reports the overall score.
### External configuration management tools
Tools like [Kustomize](https://github.com/kubernetes-sigs/kustomize) can be used to manage variations in configurations outside of clusters. There are several advantages to this approach when used to produce variations of the same base configuration. However, such solutions cannot be used to validate or enforce configurations.
## Status
*Kyverno is under active development and not ready for production use. Key components and policy definitions are likely to change as we complete core features.*
## Documentation
* [Getting Started](documentation/installation.md)
@ -148,12 +154,19 @@ Here are some the major features we plan on completing before a 1.0 release:
* [Events](https://github.com/nirmata/kyverno/issues/14)
* [Policy Violations](https://github.com/nirmata/kyverno/issues/24)
* [Generate any resource](https://github.com/nirmata/kyverno/issues/21)
* [Conditionals on existing resources](https://github.com/nirmata/kyverno/issues/57)
* [Extend CLI to operate on cluster resources ](https://github.com/nirmata/kyverno/issues/25)
* [Extend CLI to operate on cluster resources ](https://github.com/nirmata/kyverno/issues/164)
## Getting help
* For feature requests and bugs, file an [issue](https://github.com/nirmata/kyverno/issues).
* For discussions or questions, join the [mailing list](https://groups.google.com/forum/#!forum/kyverno)
## Contributing
Welcome to our community and thanks for contributing!
* Please review and agree to abide with the [Code of Conduct](/CODE_OF_CONDUCT.md) before contributing.
* See the [Wiki](https://github.com/nirmata/kyverno/wiki) for developer documentation.
* Browse through the [open issues](https://github.com/nirmata/kyverno/issues)

View file

@ -176,7 +176,7 @@ spec:
containers:
- name: kyverno
image: nirmata/kyverno:latest
imagePullPolicy: IfNotPresent
args: ["--filterKind","Nodes,Events,APIService,SubjectAccessReview"]
ports:
- containerPort: 443
securityContext:

View file

@ -0,0 +1,126 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: policies.kyverno.io
spec:
group: kyverno.io
versions:
- name: v1alpha1
served: true
storage: true
scope: Cluster
names:
kind: Policy
plural: policies
singular: policy
subresources:
status: {}
validation:
openAPIV3Schema:
properties:
spec:
required:
- rules
properties:
rules:
type: array
items:
type: object
required:
- name
- resource
properties:
name:
type: string
resource:
type: object
required:
- kinds
properties:
kinds:
type: array
items:
type: string
name:
type: string
selector:
properties:
matchLabels:
type: object
additionalProperties:
type: string
matchExpressions:
type: array
items:
type: object
required:
- key
- operator
properties:
key:
type: string
operator:
type: string
values:
type: array
items:
type: string
mutate:
type: object
properties:
overlay:
AnyValue: {}
patches:
type: array
items:
type: object
required:
- path
- op
properties:
path:
type: string
op:
type: string
enum:
- add
- replace
- remove
value:
AnyValue: {}
validate:
type: object
required:
- pattern
properties:
message:
type: string
pattern:
AnyValue: {}
generate:
type: object
required:
- kind
- name
properties:
kind:
type: string
name:
type: string
clone:
type: object
required:
- namespace
- name
properties:
namespace:
type: string
name:
type: string
data:
AnyValue: {}
---
kind: Namespace
apiVersion: v1
metadata:
name: "kyverno"

View file

@ -112,19 +112,25 @@ kubectl logs <kyverno-pod-name> -n kyverno
Here is a script that generates a self-signed CA, a TLS certificate-key pair, and the corresponding kubernetes secrets: [helper script](/scripts/generate-self-signed-cert-and-k8secrets.sh)
# Installing in a Development Environment
# Installing outside of the cluster (debug mode)
To build and run Kyverno in a development environment see: https://github.com/nirmata/kyverno/wiki/Building
To build Kyverno in a development environment see: https://github.com/nirmata/kyverno/wiki/Building
To check if the controller is working, find it in the list of kyverno pods:
To run controller in this mode you should prepare TLS key/certificate pair for debug webhook, then start controller with kubeconfig and the server address.
`kubectl get pods -n kyverno`
1. Run scripts/deploy-controller-debug.sh --service=localhost --serverIP=<server_IP>, where <server_IP> is the IP address of the host where controller runs. This scripts will generate TLS certificate for debug webhook server and register this webhook in the cluster. Also it registers CustomResource Policy.
2. Start the controller using the following command: sudo kyverno --kubeconfig=~/.kube/config --serverIP=<server_IP>
# Try Kyverno without a Kubernetes cluster
The [Kyverno CLI](documentation/testing-policies-cli.md) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster.
The [Kyverno CLI](documentation/testing-policies.md#test-using-the-kyverno-cli) allows you to write and test policies without installing Kyverno in a Kubernetes cluster. Some features are not supported without a Kubernetes cluster.
# Filter kuberenetes resources that admission webhook should not process
The admission webhook checks if a policy is applicable on all admission requests. The kubernetes kinds that are not be processed can be filtered by using the command line argument 'filterKind'.
By default we have specified Nodes, Events, APIService & SubjectAccessReview as the kinds to be skipped in the [install.yaml](https://github.com/nirmata/kyverno/raw/master/definitions/install.yaml).
---
<small>*Read Next >> [Writing Policies](/documentation/writing-policies.md)*</small>

View file

@ -22,7 +22,7 @@ kubectl get -f CM.yaml -o yaml
## Test using the Kyverno CLI
The Kyverno Command Line Interface (CLI) tool enables writing and testing policies without requiring Kubernetes clusters and without having to apply local policy changes to a cluster.
The Kyverno Command Line Interface (CLI) tool allows writing and testing policies without having to apply local policy changes to a cluster. You can also test policies without a Kubernetes clusters, but results may vary as default values will not be filled in.
### Building the CLI
@ -49,14 +49,22 @@ go get -u https://github.com/nirmata/kyverno/cmd/kyverno
### Using the CLI
The CLI loads default kubeconfig ($HOME/.kube/config) to test policies in Kubernetes cluster. If no kubeconfig is found, the CLI will test policies on raw resources.
To test a policy using the CLI type:
`kyverno <policy> <resource YAML file or folder>`
`kyverno apply @<policy> @<resource YAML file or folder>`
For example:
```bash
kyverno ../../examples/cli/policy-deployment.yaml ../../examples/cli/resources
kyverno apply @../../examples/cli/policy-deployment.yaml @../../examples/cli/resources
```
In future releases, the CLI will support complete validation of policies and will allow testing policies against resources in Kubernetes clusters.
To test a policy with the specific kubeconfig:
```bash
kyverno apply @../../examples/cli/policy-deployment.yaml @../../examples/cli/resources --kubeconfig $PATH_TO_KUBECONFIG_FILE
```
In future releases, the CLI will support complete validation and generation of policies.

View file

@ -2,7 +2,7 @@
# Generate Configurations
```generate``` feature can be applied to created namespaces to create new resources in them. This feature is useful when every namespace in a cluster must contain some basic required resources. The feature is available for policy rules in which the resource kind is Namespace.
```generate``` is used to create default resources for a namespace. This feature is useful for managing resources that are required in each namespace.
## Example 1
@ -46,8 +46,8 @@ spec:
````
In this example, when this policy is applied, any new namespace that satisfies the label selector will receive 2 new resources after its creation:
* ConfigMap copied from default/config-template.
* Secret with values DB_USER and DB_PASSWORD, and label ```purpose: mongo```.
* ConfigMap copied from default/config-template.
* Secret with values DB_USER and DB_PASSWORD, and label ```purpose: mongo```.
## Example 2
@ -73,11 +73,11 @@ spec:
matchExpressions: []
policyTypes: []
metadata:
annotations: {}
labels:
policyname: "default"
````
In this example, when this policy is applied, any new namespace will receive a new NetworkPolicy resource based on the specified template that by default denies all inbound and outbound traffic.
In this example, when the policy is applied, any new namespace will receive a nNtworkPolicy based on the specified template that by default denies all inbound and outbound traffic.
---
<small>*Read Next >> [Testing Policies](/documentation/testing-policies.md)*</small>

View file

@ -2,13 +2,13 @@
# Mutate Configurations
The ```mutate``` rule contains actions that should be applied to the resource before its creation. Mutation can be made using patches or overlay. Using ```patches``` in the JSONPatch format, you can make point changes to the created resource, and ```overlays``` are designed to bring the resource to the desired view according to a specific pattern.
The ```mutate``` rule contains actions that will be applied to matching resource before their creation. A mutate rule can be written as a JSON Patch or as an overlay. By using a ```patch``` in the (JSONPatch - RFC 6902)[http://jsonpatch.com/] format, you can make precise changes to the resource being created. Using an ```overlay``` is convenient for describing the desired state of the resource.
Resource mutation occurs before validation, so the validation rules should not contradict the changes set in the mutation section.
Resource mutation occurs before validation, so the validation rules should not contradict the changes performed by the mutation section.
## Patches
The patches are used to make direct changes in the created resource. In the next example the patch will be applied to all Deployments that contain a word "nirmata" in the name.
This patch adds an init container to all deployments.
````yaml
apiVersion : kyverno.io/v1alpha1
@ -17,29 +17,27 @@ metadata :
name : policy-v1
spec :
rules:
- name: "Deployment of *nirmata* images"
- name: "add-init-secrets"
resource:
kind: Deployment
# Name is optional. By default validation policy is applicable to any resource of supported kind.
# Name supports wildcards * and ?
name: "*nirmata*"
kinds:
- Deployment
mutate:
patches:
# This patch adds sidecar container to every deployment that matches this policy
- path: "/spec/template/spec/containers/0/"
- path: "/spec/template/spec/initContainers/0/"
op: add
value:
- image: "nirmata.io/sidecar:latest"
imagePullPolicy: "Always"
ports:
- containerPort: 443
- image: "nirmata.io/kube-vault-client:v2"
name: "init-secrets"
````
There is one patch in the rule, it will add the new image to the "containers" list with specified parameters. Patch is described in [JSONPatch](http://jsonpatch.com/) format and support the operations ('op' field):
[JSONPatch](http://jsonpatch.com/) supports the following operations (in the 'op' field):
* **add**
* **replace**
* **remove**
Here is the example with of a patch which removes a label from the secret:
With Kyverno, the add and replace have the same behavior i.e. both operations will add or replace the target element.
Here is the example of a patch that removes a label from the secret:
````yaml
apiVersion : kyverno.io/v1alpha1
kind : Policy
@ -49,7 +47,6 @@ spec :
rules:
- name: "Remove unwanted label"
resource:
# Will be applied to all secrets, because name and selector are not specified
kind: Secret
mutate:
patches:
@ -61,9 +58,9 @@ Note, that if **remove** operation cannot be applied, then this **remove** opera
## Overlay
The Mutation Overlay is the desired form of resource. The existing resource parameters are replaced with the parameters described in the overlay. If there are no such parameters in the target resource, they are copied to the resource from the overlay. The overlay is not used to delete the properties of a resource: use **patches** for this purpose.
An mutation overlay describes the desired form of resource. The existing resource values are replaced with the values specified in the overlay. If a value is specified in the overlay but not present in the target resource, then it will be added to the resource. The overlay cannot be used to delete values in a resource: use **patches** for this purpose.
The next overlay will add or change the hard limit for memory to 2 gigabytes in every ResourceQuota with label ```quota: low```:
The following mutation overlay will add (or replace) the memory request and limit to 10Gi for every Pod with a label ```memory: high```:
````yaml
apiVersion : kyverno.io/v1alpha1
@ -74,32 +71,36 @@ spec :
rules:
- name: "Set hard memory limit to 2Gi"
resource:
# Will be applied to all secrets, because name and selector are not specified
kind: ResourceQuota
kind: Pod
selector:
matchLabels:
quota: low
memory: high
mutate:
overlay:
spec:
hard:
limits.memory: 2Gi
containers:
# the wildcard * will match all containers in the list
- name: *
resources:
requests:
memory: "10Gi"
limits:
memory: "10Gi"
````
The ```overlay``` keyword under ```mutate``` feature describes the desired form of ResourceQuota.
### Working with lists
The application of an overlay to the list without additional settings is pretty straightforward: the new items will be added to the list exсept of those that totally equal to existent items. For example, the next overlay will add IP "192.168.10.172" to all addresses in all Endpoints:
Applying overlays to a list type without is fairly straightforward: new items will be added to the list, unless they already ecist. For example, the next overlay will add IP "192.168.10.172" to all addresses in all Endpoints:
````yaml
apiVersion: policy.nirmata.io/v1alpha1
kind: Policy
metadata:
name: policy-endpoints-
name: policy-endpoints
spec:
rules:
- resource:
# Applied to all endpoints
kind : Endpoints
mutate:
overlay:
@ -108,16 +109,21 @@ spec:
- ip: 192.168.10.172
````
You can use overlays to merge objects inside lists using **anchor** items marked by parentheses. For example, this overlay will add/replace port to 6443 in all ports with name that start from the word "secure":
### Conditional logic using anchors
An **anchor** field, marked by parentheses, allows conditional processing of configurations. Processing stops when the anchor value does not match. Once processing stops, any child elements or any remaining siblings in a list, will not be processed.
For example, this overlay will add or replace the value 6443 for the port field, for all ports with a name value that starts with "secure":
````yaml
apiVersion : policy.nirmata.io/v1alpha1
kind : Policy
metadata :
name : policy-endpoints-should-be-more-secure
name : policy-set-port
spec :
rules:
- resource:
# Applied to all endpoints
kind : Endpoints
mutate:
overlay:
@ -127,13 +133,36 @@ spec :
port: 6443
````
The **anchors** marked in parentheses support **wildcards**:
The **anchors** values support **wildcards**:
1. `*` - matches zero or more alphanumeric characters
2. `?` - matches a single alphanumeric character
## Details
The behavior of overlays described more detailed in the project's wiki: [Mutation Overlay](https://github.com/nirmata/kyverno/wiki/Mutation-Overlay)
### Add if not present
A variation of an anchor, is to add a field value if it is not already defined. This is done by using the ````+(...)```` notation for the field.
For example, this overlay will set the port to 6443, if a port is not already defined:
````yaml
apiVersion : policy.nirmata.io/v1alpha1
kind : Policy
metadata :
name : policy-set-port
spec :
rules:
- resource:
kind : Endpoints
mutate:
overlay:
subsets:
- ports:
+(port): 6443
````
## Additional Details
Additional details on mutation overlay behaviors are available on the wiki: [Mutation Overlay](https://github.com/nirmata/kyverno/wiki/Mutation-Overlay)
---
<small>*Read Next >> [Validate](/documentation/writing-policies-validate.md)*</small>
<small>*Read Next >> [Generate](/documentation/writing-policies-generate.md)*</small>

View file

@ -43,24 +43,26 @@ metadata :
name : validation-example
spec :
rules:
- resource:
- name: check-label
resource:
# Kind specifies one or more resource types to match
kinds:
- Deployment
- StatefuleSet
- DaemonSet
# Name is optional and can use wildcards
name: *
name: "*"
# Selector is optional
selector:
validate:
# Message is optional
message: "The label app is required"
message: "The label app is required"
pattern:
spec:
selector:
matchLabels:
app: "?*"
template:
metadata:
labels:
app: "?*"
````
@ -68,4 +70,4 @@ Additional examples are available in [examples](/examples/)
---
<small>*Read Next >> [Generate](/documentation/writing-policies-generate.md)*</small>
<small>*Read Next >> [Generate](/documentation/writing-policies-mutate.md)*</small>

View file

@ -10,29 +10,25 @@ kind : Policy
metadata :
name : policy
spec :
# Each policy has a list of rules applied in declaration order
rules:
# Rules must have a name
- name: "check-pod-controller-labels"
# Rules must have a unique name
- name: "check-pod-controller-labels"
# Each rule matches specific resource described by "resource" field.
resource:
kind: Deployment, StatefulSet, DaemonSet
# Name is optional. By default validation policy is applicable to any resource of supported kinds.
# Name supports wildcards * and ?
kinds:
- Deployment
- StatefulSet
- DaemonSet
# A resource name is optional. Name supports wildcards * and ?
name: "*"
# Selector is optional and can be used to match specific resources
# Selector values support wildcards * and ?
# A resoucre selector is optional. Selector values support wildcards * and ?
selector:
# A selector can use match
matchLabels:
app: mongodb
matchExpressions:
- {key: tier, operator: In, values: [database]}
# Each rule can contain a single validate, mutate, or generate directive
...
````

View file

@ -8,10 +8,9 @@ relativeURLs=true
[params]
description = "Kubernetes Native Policy Management"
long_description = '''
Manage policies as Kuberneres resources using YAML or JSON. Easily validate,
mutate, or generate Kubernetes resources. Match resources based on label selectors
and wildcards. View policy results as events, and policy violations as events or
in policy status.'''
Manage policies as Kubernetes resources. Validate, mutate, and generate configurations.
Select resources based on labels and wildcards. View policy enforcement as events. Detect
policy violations for existing resources.'''
author_name = "Nirmata"
author_url = "https://nirmata.com"
project_url = "https://github.com/nirmata/kyverno/"

10
init.go
View file

@ -1,6 +1,8 @@
package main
import (
"fmt"
"github.com/golang/glog"
client "github.com/nirmata/kyverno/pkg/dclient"
tls "github.com/nirmata/kyverno/pkg/tls"
@ -40,10 +42,12 @@ func initTLSPemPair(configuration *rest.Config, client *client.Client) (*tls.Tls
if err != nil {
return nil, err
}
err = client.WriteTlsPair(certProps, tlsPair)
if err != nil {
glog.Errorf("Unable to save TLS pair to the cluster: %v", err)
if err = client.WriteTlsPair(certProps, tlsPair); err != nil {
return nil, fmt.Errorf("Unable to save TLS pair to the cluster: %v", err)
}
return tlsPair, nil
}
glog.Infoln("Using existing TLS key/certificate pair")
return tlsPair, nil
}

11
main.go
View file

@ -15,7 +15,9 @@ import (
)
var (
kubeconfig string
kubeconfig string
serverIP string
filterK8Kinds webhooks.ArrayFlags
)
func main() {
@ -49,13 +51,12 @@ func main() {
if err != nil {
glog.Fatalf("Failed to initialize TLS key/certificate pair: %v\n", err)
}
server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory)
server, err := webhooks.NewWebhookServer(client, tlsPair, policyInformerFactory, filterK8Kinds)
if err != nil {
glog.Fatalf("Unable to create webhook server: %v\n", err)
}
webhookRegistrationClient, err := webhooks.NewWebhookRegistrationClient(clientConfig, client)
webhookRegistrationClient, err := webhooks.NewWebhookRegistrationClient(clientConfig, client, serverIP)
if err != nil {
glog.Fatalf("Unable to register admission webhooks on cluster: %v\n", err)
}
@ -81,6 +82,8 @@ func main() {
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&serverIP, "serverIP", "", "IP address where Kyverno controller runs. Only required if out-of-cluster.")
flag.Var(&filterK8Kinds, "filterKind", "k8 kind where policy is not evaluated by the admission webhook. example --filterKind \"Event\" --filterKind \"TokenReview,ClusterRole\"")
config.LogDefaultFlags()
flag.Parse()
}

View file

@ -7,11 +7,13 @@ const (
KubePolicyNamespace = "kyverno"
WebhookServiceName = "kyverno-svc"
MutatingWebhookConfigurationName = "kyverno-mutating-webhook-cfg"
MutatingWebhookName = "nirmata.kyverno.mutating-webhook"
MutatingWebhookConfigurationName = "kyverno-mutating-webhook-cfg"
MutatingWebhookConfigurationDebug = "kyverno-mutating-webhook-cfg-debug"
MutatingWebhookName = "nirmata.kyverno.mutating-webhook"
ValidatingWebhookConfigurationName = "kyverno-validating-webhook-cfg"
ValidatingWebhookName = "nirmata.kyverno.validating-webhook"
ValidatingWebhookConfigurationName = "kyverno-validating-webhook-cfg"
ValidatingWebhookConfigurationDebug = "kyverno-validating-webhook-cfg-debug"
ValidatingWebhookName = "nirmata.kyverno.validating-webhook"
// Due to kubernetes issue, we must use next literal constants instead of deployment TypeMeta fields
// Issue: https://github.com/kubernetes/kubernetes/pull/63972

View file

@ -79,10 +79,12 @@ func (f *fixture) setupFixture() {
if err != nil {
f.t.Fatal(err)
}
regResources := []schema.GroupVersionResource{
schema.GroupVersionResource{Group: "kyverno.io/", Version: "v1alpha1", Resource: "policys"}}
fclient.SetDiscovery(client.NewFakeDiscoveryClient(regResources))
regresource := []schema.GroupVersionResource{
schema.GroupVersionResource{Group: "kyverno.io",
Version: "v1alpha1",
Resource: "policys"}}
fclient.SetDiscovery(client.NewFakeDiscoveryClient(regresource))
}
func newPolicy(name string) *types.Policy {

View file

@ -58,7 +58,7 @@ func (c *Client) submitAndApproveCertificateRequest(req *certificates.Certificat
for _, csr := range csrList.Items {
if csr.GetName() == req.ObjectMeta.Name {
err := c.DeleteResouce(CSRs, "", csr.GetName())
err := c.DeleteResouce(CSRs, "", csr.GetName(), false)
if err != nil {
return nil, fmt.Errorf("Unable to delete existing certificate request: %v", err)
}
@ -67,7 +67,7 @@ func (c *Client) submitAndApproveCertificateRequest(req *certificates.Certificat
}
}
unstrRes, err := c.CreateResource(CSRs, "", req)
unstrRes, err := c.CreateResource(CSRs, "", req, false)
if err != nil {
return nil, err
}
@ -209,7 +209,7 @@ func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPem
Type: v1.SecretTypeTLS,
}
_, err := c.CreateResource(Secrets, props.Namespace, secret)
_, err := c.CreateResource(Secrets, props.Namespace, secret, false)
if err == nil {
glog.Infof("Secret %s is created", name)
}
@ -223,7 +223,7 @@ func (c *Client) WriteTlsPair(props tls.TlsCertificateProps, pemPair *tls.TlsPem
secret.Data[v1.TLSCertKey] = pemPair.Certificate
secret.Data[v1.TLSPrivateKeyKey] = pemPair.PrivateKey
_, err = c.UpdateResource(Secrets, props.Namespace, secret)
_, err = c.UpdateResource(Secrets, props.Namespace, secret, false)
if err != nil {
return err
}

View file

@ -118,34 +118,50 @@ func (c *Client) ListResource(resource string, namespace string) (*unstructured.
}
// DeleteResouce deletes the specified resource
func (c *Client) DeleteResouce(resource string, namespace string, name string) error {
return c.getResourceInterface(resource, namespace).Delete(name, &meta.DeleteOptions{})
func (c *Client) DeleteResouce(resource string, namespace string, name string, dryRun bool) error {
options := meta.DeleteOptions{}
if dryRun {
options = meta.DeleteOptions{DryRun: []string{meta.DryRunAll}}
}
return c.getResourceInterface(resource, namespace).Delete(name, &options)
}
// CreateResource creates object for the specified resource/namespace
func (c *Client) CreateResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) {
func (c *Client) CreateResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) {
options := meta.CreateOptions{}
if dryRun {
options = meta.CreateOptions{DryRun: []string{meta.DryRunAll}}
}
// convert typed to unstructured obj
if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil {
return c.getResourceInterface(resource, namespace).Create(unstructuredObj, meta.CreateOptions{})
return c.getResourceInterface(resource, namespace).Create(unstructuredObj, options)
}
return nil, fmt.Errorf("Unable to create resource ")
}
// UpdateResource updates object for the specified resource/namespace
func (c *Client) UpdateResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) {
func (c *Client) UpdateResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) {
options := meta.UpdateOptions{}
if dryRun {
options = meta.UpdateOptions{DryRun: []string{meta.DryRunAll}}
}
// convert typed to unstructured obj
if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil {
return c.getResourceInterface(resource, namespace).Update(unstructuredObj, meta.UpdateOptions{})
return c.getResourceInterface(resource, namespace).Update(unstructuredObj, options)
}
return nil, fmt.Errorf("Unable to update resource ")
}
// UpdateStatusResource updates the resource "status" subresource
func (c *Client) UpdateStatusResource(resource string, namespace string, obj interface{}) (*unstructured.Unstructured, error) {
func (c *Client) UpdateStatusResource(resource string, namespace string, obj interface{}, dryRun bool) (*unstructured.Unstructured, error) {
options := meta.UpdateOptions{}
if dryRun {
options = meta.UpdateOptions{DryRun: []string{meta.DryRunAll}}
}
// convert typed to unstructured obj
if unstructuredObj := convertToUnstructured(obj); unstructuredObj != nil {
return c.getResourceInterface(resource, namespace).UpdateStatus(unstructuredObj, meta.UpdateOptions{})
return c.getResourceInterface(resource, namespace).UpdateStatus(unstructuredObj, options)
}
return nil, fmt.Errorf("Unable to update resource ")
}
@ -193,7 +209,7 @@ func (c *Client) GenerateResource(generator types.Generation, namespace string)
glog.Errorf("Can't create a resource %s: %v", generator.Name, err)
return nil
}
_, err = c.CreateResource(rGVR.Resource, namespace, resource)
_, err = c.CreateResource(rGVR.Resource, namespace, resource, false)
if err != nil {
return err
}

View file

@ -1,13 +1,11 @@
package client
import (
"fmt"
"testing"
policytypes "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@ -22,27 +20,13 @@ import (
// - kubernetes client
// - objects to initialize the client
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
},
}
type fixture struct {
t *testing.T
objects []runtime.Object
client *Client
}
func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[string]interface{}) *unstructured.Unstructured {
u := newUnstructured(apiVersion, kind, namespace, name)
u.Object["spec"] = spec
return u
}
func TestClient(t *testing.T) {
scheme := runtime.NewScheme()
func newFixture(t *testing.T) *fixture {
// init groupversion
regResource := []schema.GroupVersionResource{
schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"},
@ -50,7 +34,7 @@ func TestClient(t *testing.T) {
schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"},
schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
}
// init resources
objects := []runtime.Object{newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
@ -58,8 +42,8 @@ func TestClient(t *testing.T) {
newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"),
newUnstructured("apps/v1", "Deployment", "kyverno", "kyverno-deployment"),
}
// Mock Client
scheme := runtime.NewScheme()
// Create mock client
client, err := NewMockClient(scheme, objects...)
if err != nil {
t.Fatal(err)
@ -67,97 +51,113 @@ func TestClient(t *testing.T) {
// set discovery Client
client.SetDiscovery(NewFakeDiscoveryClient(regResource))
f := fixture{
t: t,
objects: objects,
client: client,
}
return &f
}
func TestCRUDResource(t *testing.T) {
f := newFixture(t)
// Get Resource
res, err := client.GetResource("thekinds", "ns-foo", "name-foo")
_, err := f.client.GetResource("thekinds", "ns-foo", "name-foo")
if err != nil {
t.Fatal(err)
t.Errorf("GetResource not working: %s", err)
}
fmt.Println(res)
// List Resources
list, err := client.ListResource("thekinds", "ns-foo")
_, err = f.client.ListResource("thekinds", "ns-foo")
if err != nil {
t.Fatal(err)
t.Errorf("ListResource not working: %s", err)
}
fmt.Println(len(list.Items))
// DeleteResouce
err = client.DeleteResouce("thekinds", "ns-foo", "name-bar")
err = f.client.DeleteResouce("thekinds", "ns-foo", "name-bar", false)
if err != nil {
t.Fatal(err)
t.Errorf("DeleteResouce not working: %s", err)
}
// CreateResource
res, err = client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"))
_, err = f.client.CreateResource("thekinds", "ns-foo", newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"), false)
if err != nil {
t.Fatal(err)
t.Errorf("CreateResource not working: %s", err)
}
// UpdateResource
res, err = client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"}))
_, err = f.client.UpdateResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "bar"}), false)
if err != nil {
t.Fatal(err)
t.Errorf("UpdateResource not working: %s", err)
}
// UpdateStatusResource
res, err = client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"}))
_, err = f.client.UpdateStatusResource("thekinds", "ns-foo", newUnstructuredWithSpec("group/version", "TheKind", "ns-foo", "name-foo1", map[string]interface{}{"foo": "status"}), false)
if err != nil {
t.Fatal(err)
t.Errorf("UpdateStatusResource not working: %s", err)
}
}
iEvent, err := client.GetEventsInterface()
func TestEventInterface(t *testing.T) {
f := newFixture(t)
iEvent, err := f.client.GetEventsInterface()
if err != nil {
t.Fatal(err)
t.Errorf("GetEventsInterface not working: %s", err)
}
eventList, err := iEvent.List(meta.ListOptions{})
_, err = iEvent.List(meta.ListOptions{})
if err != nil {
t.Fatal(err)
t.Errorf("Testing Event interface not working: %s", err)
}
fmt.Println(eventList.Items)
iCSR, err := client.GetCSRInterface()
if err != nil {
t.Fatal(err)
}
csrList, err := iCSR.List(meta.ListOptions{})
if err != nil {
t.Fatal(err)
}
fmt.Println(csrList.Items)
}
func TestCSRInterface(t *testing.T) {
f := newFixture(t)
iCSR, err := f.client.GetCSRInterface()
if err != nil {
t.Errorf("GetCSRInterface not working: %s", err)
}
_, err = iCSR.List(meta.ListOptions{})
if err != nil {
t.Errorf("Testing CSR interface not working: %s", err)
}
}
func TestGenerateResource(t *testing.T) {
f := newFixture(t)
//GenerateResource -> copy From
// 1 create namespace
// 2 generate resource
// create namespace
ns, err := client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1"))
ns, err := f.client.CreateResource("namespaces", "", newUnstructured("v1", "Namespace", "", "ns1"), false)
if err != nil {
t.Fatal(err)
t.Errorf("CreateResource not working: %s", err)
}
gen := policytypes.Generation{Kind: "TheKind",
Name: "gen-kind",
Clone: &policytypes.CloneFrom{Namespace: "ns-foo", Name: "name-foo"}}
err = client.GenerateResource(gen, ns.GetName())
err = f.client.GenerateResource(gen, ns.GetName())
if err != nil {
t.Fatal(err)
t.Errorf("GenerateResource not working: %s", err)
}
res, err = client.GetResource("thekinds", "ns1", "gen-kind")
_, err = f.client.GetResource("thekinds", "ns1", "gen-kind")
if err != nil {
t.Fatal(err)
t.Errorf("GetResource not working: %s", err)
}
// GenerateResource -> data
gen = policytypes.Generation{Kind: "TheKind",
Name: "name2-baz-new",
Data: newUnstructured("group2/version", "TheKind", "ns1", "name2-baz-new")}
err = client.GenerateResource(gen, ns.GetName())
err = f.client.GenerateResource(gen, ns.GetName())
if err != nil {
t.Fatal(err)
t.Errorf("GenerateResource not working: %s", err)
}
res, err = client.GetResource("thekinds", "ns1", "name2-baz-new")
_, err = f.client.GetResource("thekinds", "ns1", "name2-baz-new")
if err != nil {
t.Fatal(err)
t.Errorf("GetResource not working: %s", err)
}
}
func TestKubePolicyDeployment(t *testing.T) {
f := newFixture(t)
_, err := f.client.GetKubePolicyDeployment()
if err != nil {
t.Fatal(err)
}
// Get Kube Policy Deployment
deploy, err := client.GetKubePolicyDeployment()
if err != nil {
t.Fatal(err)
}
fmt.Println(deploy.GetName())
}

View file

@ -4,6 +4,7 @@ import (
"strings"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic/fake"
@ -70,3 +71,41 @@ func (c *fakeDiscoveryClient) getGVRFromKind(kind string) schema.GroupVersionRes
resource := strings.ToLower(kind) + "s"
return c.getGVR(resource)
}
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
},
}
}
func newUnstructuredWithSpec(apiVersion, kind, namespace, name string, spec map[string]interface{}) *unstructured.Unstructured {
u := newUnstructured(apiVersion, kind, namespace, name)
u.Object["spec"] = spec
return u
}
func retry(attempts int, sleep time.Duration, fn func() error) error {
if err := fn(); err != nil {
if s, ok := err.(stop); ok {
return s.error
}
if attempts--; attempts > 0 {
time.Sleep(sleep)
return retry(attempts, 2*sleep, fn)
}
return err
}
return nil
}
// Custom error
type stop struct {
error
}

160
pkg/engine/anchor.go Normal file
View file

@ -0,0 +1,160 @@
package engine
import (
"strconv"
"github.com/nirmata/kyverno/pkg/result"
)
// CreateAnchorHandler is a factory that create anchor handlers
func CreateAnchorHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler {
switch {
case isConditionAnchor(anchor):
return NewConditionAnchorValidationHandler(anchor, pattern, path)
case isExistanceAnchor(anchor):
return NewExistanceAnchorValidationHandler(anchor, pattern, path)
default:
return NewNoAnchorValidationHandler(path)
}
}
// ValidationAnchorHandler is an interface that represents
// a family of anchor handlers for array of maps
// resourcePart must be an array of dictionaries
// patternPart must be a dictionary with anchors
type ValidationAnchorHandler interface {
Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult
}
// NoAnchorValidationHandler just calls validateMap
// because no anchors were found in the pattern map
type NoAnchorValidationHandler struct {
path string
}
// NewNoAnchorValidationHandler creates new instance of
// NoAnchorValidationHandler
func NewNoAnchorValidationHandler(path string) ValidationAnchorHandler {
return &NoAnchorValidationHandler{
path: path,
}
}
// Handle performs validation in context of NoAnchorValidationHandler
func (navh *NoAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult {
handlingResult := result.NewRuleApplicationResult("")
for i, resourceElement := range resourcePart {
currentPath := navh.path + strconv.Itoa(i) + "/"
typedResourceElement, ok := resourceElement.(map[string]interface{})
if !ok {
handlingResult.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternPart, resourceElement)
return handlingResult
}
res := validateMap(typedResourceElement, patternPart, currentPath)
handlingResult.MergeWith(&res)
}
return handlingResult
}
// ConditionAnchorValidationHandler performs
// validation only for array elements that
// pass condition in the anchor
// (key): value
type ConditionAnchorValidationHandler struct {
anchor string
pattern interface{}
path string
}
// NewConditionAnchorValidationHandler creates new instance of
// NoAnchorValidationHandler
func NewConditionAnchorValidationHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler {
return &ConditionAnchorValidationHandler{
anchor: anchor,
pattern: pattern,
path: path,
}
}
// Handle performs validation in context of ConditionAnchorValidationHandler
func (cavh *ConditionAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult {
_, handlingResult := handleConditionCases(resourcePart, patternPart, cavh.anchor, cavh.pattern, cavh.path)
return handlingResult
}
// ExistanceAnchorValidationHandler performs
// validation only for array elements that
// pass condition in the anchor
// AND requires an existance of at least one
// element that passes this condition
// ^(key): value
type ExistanceAnchorValidationHandler struct {
anchor string
pattern interface{}
path string
}
// NewExistanceAnchorValidationHandler creates new instance of
// NoAnchorValidationHandler
func NewExistanceAnchorValidationHandler(anchor string, pattern interface{}, path string) ValidationAnchorHandler {
return &ExistanceAnchorValidationHandler{
anchor: anchor,
pattern: pattern,
path: path,
}
}
// Handle performs validation in context of ExistanceAnchorValidationHandler
func (eavh *ExistanceAnchorValidationHandler) Handle(resourcePart []interface{}, patternPart map[string]interface{}) result.RuleApplicationResult {
anchoredEntries, handlingResult := handleConditionCases(resourcePart, patternPart, eavh.anchor, eavh.pattern, eavh.path)
if 0 == anchoredEntries {
handlingResult.FailWithMessagef("Existance anchor %s used, but no suitable entries were found", eavh.anchor)
}
return handlingResult
}
// check if array element fits the anchor
func checkForAnchorCondition(anchor string, pattern interface{}, resourceMap map[string]interface{}) bool {
anchorKey := removeAnchor(anchor)
if value, ok := resourceMap[anchorKey]; ok {
return ValidateValueWithPattern(value, pattern)
}
return false
}
// both () and ^() are checking conditions and have a lot of similar logic
// the only difference is that ^() requires existace of one element
// anchoredEntries var counts this occurences.
func handleConditionCases(resourcePart []interface{}, patternPart map[string]interface{}, anchor string, pattern interface{}, path string) (int, result.RuleApplicationResult) {
handlingResult := result.NewRuleApplicationResult("")
anchoredEntries := 0
for i, resourceElement := range resourcePart {
currentPath := path + strconv.Itoa(i) + "/"
typedResourceElement, ok := resourceElement.(map[string]interface{})
if !ok {
handlingResult.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternPart, resourceElement)
break
}
if !checkForAnchorCondition(anchor, pattern, typedResourceElement) {
continue
}
anchoredEntries++
res := validateMap(typedResourceElement, patternPart, currentPath)
handlingResult.MergeWith(&res)
}
return anchoredEntries, handlingResult
}

View file

@ -24,182 +24,258 @@ func ProcessOverlay(rule kubepolicy.Rule, rawResource []byte, gvk metav1.GroupVe
var appliedPatches []PatchBytes
json.Unmarshal(rawResource, &resource)
patch := applyOverlay(resource, *rule.Mutation.Overlay, "/", &overlayApplicationResult)
patches, res := mutateResourceWithOverlay(resource, *rule.Mutation.Overlay)
overlayApplicationResult.MergeWith(&res)
if overlayApplicationResult.GetReason() == result.Success {
appliedPatches = append(appliedPatches, patch...)
appliedPatches = append(appliedPatches, patches...)
}
return appliedPatches, overlayApplicationResult
}
// goes down through overlay and resource trees and applies overlay
func applyOverlay(resource, overlay interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
// mutateResourceWithOverlay is a start of overlaying process
func mutateResourceWithOverlay(resource, pattern interface{}) ([]PatchBytes, result.RuleApplicationResult) {
// It assumes that mutation is started from root, so "/" is passed
return applyOverlay(resource, pattern, "/")
}
// applyOverlay detects type of current item and goes down through overlay and resource trees applying overlay
func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
overlayResult := result.NewRuleApplicationResult("")
// resource item exists but has different type - replace
// all subtree within this path by overlay
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
patch := replaceSubtree(overlay, path, res)
if res.Reason == result.Success {
patch, res := replaceSubtree(overlay, path)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
return appliedPatches
return appliedPatches, overlayResult
}
return applyOverlayForSameTypes(resource, overlay, path)
}
// applyOverlayForSameTypes is applyOverlay for cases when TypeOf(resource) == TypeOf(overlay)
func applyOverlayForSameTypes(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
overlayResult := result.NewRuleApplicationResult("")
// detect the type of resource and overlay and select corresponding handler
switch typedOverlay := overlay.(type) {
// map
case map[string]interface{}:
typedResource := resource.(map[string]interface{})
patches, res := applyOverlayToMap(typedResource, typedOverlay, path)
overlayResult.MergeWith(&res)
for key, value := range typedOverlay {
if wrappedWithParentheses(key) {
continue
}
currentPath := path + key + "/"
resourcePart, ok := typedResource[key]
if ok {
patches := applyOverlay(resourcePart, value, currentPath, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patches...)
}
} else {
patch := insertSubtree(value, currentPath, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patch)
}
appliedPatches = append(appliedPatches, patch)
}
}
case []interface{}:
typedResource := resource.([]interface{})
patches := applyOverlayToArray(typedResource, typedOverlay, path, res)
if res.Reason == result.Success {
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
// array
case []interface{}:
typedResource := resource.([]interface{})
patches, res := applyOverlayToArray(typedResource, typedOverlay, path)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
// elementary types
case string, float64, int64, bool:
patch := replaceSubtree(overlay, path, res)
if res.Reason == result.Success {
patch, res := replaceSubtree(overlay, path)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
default:
res.FailWithMessagef("Overlay has unsupported type: %T", overlay)
return nil
overlayResult.FailWithMessagef("Overlay has unsupported type: %T", overlay)
return nil, overlayResult
}
return appliedPatches
return appliedPatches, overlayResult
}
// for each overlay and resource array elements and applies overlay
func applyOverlayToArray(resource, overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
// for each overlay and resource map elements applies overlay
func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
if len(overlay) == 0 {
res.FailWithMessagef("Empty array detected in the overlay")
return nil
overlayResult := result.NewRuleApplicationResult("")
for key, value := range overlayMap {
// skip anchor element because it has condition, not
// the value that must replace resource value
if isConditionAnchor(key) {
continue
}
noAnchorKey := removeAnchor(key)
currentPath := path + noAnchorKey + "/"
resourcePart, ok := resourceMap[noAnchorKey]
if ok && !isAddingAnchor(key) {
// Key exists - go down through the overlay and resource trees
patches, res := applyOverlay(resourcePart, value, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
}
if !ok {
// Key does not exist - insert entire overlay subtree
patch, res := insertSubtree(value, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
}
}
if len(resource) == 0 {
return fillEmptyArray(overlay, path, res)
return appliedPatches, overlayResult
}
// for each overlay and resource array elements applies overlay
func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
overlayResult := result.NewRuleApplicationResult("")
if 0 == len(overlay) {
overlayResult.FailWithMessagef("Empty array detected in the overlay")
return nil, overlayResult
}
if 0 == len(resource) {
// If array resource is empty, insert part from overlay
patch, res := insertSubtree(overlay, path)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
return appliedPatches, res
}
if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
res.FailWithMessagef("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
return nil
overlayResult.FailWithMessagef("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
return nil, overlayResult
}
switch overlay[0].(type) {
case map[string]interface{}:
for _, overlayElement := range overlay {
typedOverlay := overlayElement.(map[string]interface{})
anchors := getAnchorsFromMap(typedOverlay)
if len(anchors) > 0 {
for i, resourceElement := range resource {
typedResource := resourceElement.(map[string]interface{})
currentPath := path + strconv.Itoa(i) + "/"
if !skipArrayObject(typedResource, anchors) {
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patches...)
}
}
}
} else if hasNestedAnchors(overlayElement) {
for i, resourceElement := range resource {
currentPath := path + strconv.Itoa(i) + "/"
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patches...)
}
}
} else {
currentPath := path + "0/"
patch := insertSubtree(overlayElement, currentPath, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patch)
}
}
}
default:
path += "0/"
for _, value := range overlay {
patch := insertSubtree(value, path, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patch)
}
}
}
return appliedPatches
return applyOverlayToArrayOfSameTypes(resource, overlay, path)
}
// In case of empty resource array
// append all non-anchor items to front
func fillEmptyArray(overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
// applyOverlayToArrayOfSameTypes applies overlay to array elements if they (resource and overlay elements) have same type
func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
if len(overlay) == 0 {
res.FailWithMessagef("Empty array detected in the overlay")
return nil
}
path += "0/"
overlayResult := result.NewRuleApplicationResult("")
switch overlay[0].(type) {
case map[string]interface{}:
for _, overlayElement := range overlay {
typedOverlay := overlayElement.(map[string]interface{})
anchors := getAnchorsFromMap(typedOverlay)
if len(anchors) == 0 {
patch := insertSubtree(overlayElement, path, res)
if res.Reason == result.Success {
appliedPatches = append(appliedPatches, patch)
}
}
}
return applyOverlayToArrayOfMaps(resource, overlay, path)
default:
for _, overlayElement := range overlay {
patch := insertSubtree(overlayElement, path, res)
if res.Reason == result.Success {
lastElementIdx := len(resource)
// Add elements to the end
for i, value := range overlay {
currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
// currentPath example: /spec/template/spec/containers/3/
patch, res := insertSubtree(value, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
}
}
return appliedPatches
return appliedPatches, overlayResult
}
func insertSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
return processSubtree(overlay, path, "add", res)
// Array of maps needs special handling as far as it can have anchors.
func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
overlayResult := result.NewRuleApplicationResult("")
lastElementIdx := len(resource)
for i, overlayElement := range overlay {
typedOverlay := overlayElement.(map[string]interface{})
anchors := getAnchorsFromMap(typedOverlay)
if len(anchors) > 0 {
// If we have anchors - choose corresponding resource element and mutate it
patches, res := applyOverlayWithAnchors(resource, overlayElement, anchors, path)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
} else if hasNestedAnchors(overlayElement) {
// If we have anchors on the lower level - continue traversing overlay and resource trees
for j, resourceElement := range resource {
currentPath := path + strconv.Itoa(j) + "/"
// currentPath example: /spec/template/spec/containers/3/
patches, res := applyOverlay(resourceElement, overlayElement, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
}
} else {
// Overlay subtree has no anchors - insert new element
currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
// currentPath example: /spec/template/spec/containers/3/
patch, res := insertSubtree(overlayElement, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patch)
}
}
}
return appliedPatches, overlayResult
}
func replaceSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
return processSubtree(overlay, path, "replace", res)
func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, anchors map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
var appliedPatches []PatchBytes
overlayResult := result.NewRuleApplicationResult("")
for i, resourceElement := range resource {
typedResource := resourceElement.(map[string]interface{})
currentPath := path + strconv.Itoa(i) + "/"
// currentPath example: /spec/template/spec/containers/3/
if !skipArrayObject(typedResource, anchors) {
patches, res := applyOverlay(resourceElement, overlay, currentPath)
overlayResult.MergeWith(&res)
if result.Success == overlayResult.GetReason() {
appliedPatches = append(appliedPatches, patches...)
}
}
}
return appliedPatches, overlayResult
}
func processSubtree(overlay interface{}, path string, op string, res *result.RuleApplicationResult) PatchBytes {
func insertSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) {
return processSubtree(overlay, path, "add")
}
func replaceSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) {
return processSubtree(overlay, path, "replace")
}
func processSubtree(overlay interface{}, path string, op string) (PatchBytes, result.RuleApplicationResult) {
overlayResult := result.NewRuleApplicationResult("")
if len(path) > 1 && path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
@ -214,76 +290,26 @@ func processSubtree(overlay interface{}, path string, op string, res *result.Rul
// check the patch
_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
if err != nil {
res.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
return nil
overlayResult.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
return nil, overlayResult
}
return PatchBytes(patchStr)
return PatchBytes(patchStr), overlayResult
}
// TODO: Overlay is already in JSON, remove this code
// converts overlay to JSON string to be inserted into the JSON Patch
func prepareJSONValue(overlay interface{}) string {
switch typed := overlay.(type) {
case map[string]interface{}:
if len(typed) == 0 {
return ""
}
jsonOverlay, err := json.Marshal(overlay)
if hasOnlyAnchors(overlay) {
return ""
}
result := ""
for key, value := range typed {
jsonValue := prepareJSONValue(value)
pair := fmt.Sprintf(`"%s":%s`, key, jsonValue)
if result != "" {
result += ", "
}
result += pair
}
result = fmt.Sprintf(`{ %s }`, result)
return result
case []interface{}:
if len(typed) == 0 {
return ""
}
if hasOnlyAnchors(overlay) {
return ""
}
result := ""
for _, value := range typed {
jsonValue := prepareJSONValue(value)
if result != "" {
result += ", "
}
result += jsonValue
}
result = fmt.Sprintf(`[ %s ]`, result)
return result
case string:
return fmt.Sprintf(`"%s"`, typed)
case float64:
return fmt.Sprintf("%f", typed)
case int64:
return fmt.Sprintf("%d", typed)
case bool:
return fmt.Sprintf("%t", typed)
default:
if err != nil || hasOnlyAnchors(overlay) {
return ""
}
return string(jsonOverlay)
}
// Anchor has pattern value, so resource shouldn't be mutated with it
// If entire subtree has only anchor keys - we should skip inserting it
func hasOnlyAnchors(overlay interface{}) bool {
switch typed := overlay.(type) {
case map[string]interface{}:
@ -296,13 +322,20 @@ func hasOnlyAnchors(overlay interface{}) bool {
return false
}
}
return true
case []interface{}:
for _, value := range typed {
if !hasOnlyAnchors(value) {
return false
}
}
default:
return false
}
return true
}
// Checks if subtree has anchors
func hasNestedAnchors(overlay interface{}) bool {
switch typed := overlay.(type) {
case map[string]interface{}:

View file

@ -2,7 +2,6 @@ package engine
import (
"encoding/json"
"github.com/nirmata/kyverno/pkg/result"
"reflect"
"testing"
@ -66,8 +65,7 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
res := result.NewRuleApplicationResult("")
patches := applyOverlay(resource, overlay, "/", &res)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, patches != nil)
@ -80,7 +78,34 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, patched != nil)
expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":444.000000,"protocol":"UDP"}]}]}`)
expectedResult := []byte(`
{
"apiVersion":"v1",
"kind":"Endpoints",
"metadata":{
"name":"test-endpoint",
"labels":{
"label":"test"
}
},
"subsets":[
{
"addresses":[
{
"ip":"192.168.10.171"
}
],
"ports":[
{
"name":"secure-connection",
"port":444.000000,
"protocol":"UDP"
}
]
}
]
}`)
compareJsonAsMap(t, expectedResult, patched)
}
@ -140,8 +165,7 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
res := result.NewRuleApplicationResult("")
patches := applyOverlay(resource, overlay, "/", &res)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, patches != nil)
@ -155,7 +179,50 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, patched != nil)
expectedResult := []byte(`{"apiVersion":"v1","kind":"Endpoints","metadata":{"name":"test-endpoint","labels":{"label":"test"}},"subsets":[{"addresses":[{"ip":"192.168.10.172"},{"ip":"192.168.10.173"}],"ports":[{"name":"insecure-connection","port":80.000000,"protocol":"UDP"}]},{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":443,"protocol":"TCP"}]}]}`)
expectedResult := []byte(`{
"apiVersion":"v1",
"kind":"Endpoints",
"metadata":{
"name":"test-endpoint",
"labels":{
"label":"test"
}
},
"subsets":[
{
"addresses":[
{
"ip":"192.168.10.171"
}
],
"ports":[
{
"name":"secure-connection",
"port":443,
"protocol":"TCP"
}
]
},
{
"addresses":[
{
"ip":"192.168.10.172"
},
{
"ip":"192.168.10.173"
}
],
"ports":[
{
"name":"insecure-connection",
"port":80,
"protocol":"UDP"
}
]
}
]
}`)
compareJsonAsMap(t, expectedResult, patched)
}
@ -219,8 +286,7 @@ func TestApplyOverlay_TestInsertToArray(t *testing.T) {
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
res := result.NewRuleApplicationResult("")
patches := applyOverlay(resource, overlay, "/", &res)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, patches != nil)
@ -303,13 +369,227 @@ func TestApplyOverlay_ImagePullPolicy(t *testing.T) {
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
res := result.NewRuleApplicationResult("")
patches := applyOverlay(resource, overlay, "/", &res)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, len(patches) != 0)
doc, err := ApplyPatches(resourceRaw, patches)
assert.NilError(t, err)
expectedResult := []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"nginx-deployment","labels":{"app":"nginx"}},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx:latest","imagePullPolicy":"IfNotPresent","name":"nginx","ports":[{"containerPort":8080.000000},{"containerPort":80}]},{"image":"ghost:latest","imagePullPolicy":"IfNotPresent","name":"ghost","ports":[{"containerPort":8080.000000}]}]}}}}`)
expectedResult := []byte(`{
"apiVersion":"apps/v1",
"kind":"Deployment",
"metadata":{
"name":"nginx-deployment",
"labels":{
"app":"nginx"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"nginx"
}
},
"template":{
"metadata":{
"labels":{
"app":"nginx"
}
},
"spec":{
"containers":[
{
"image":"nginx:latest",
"imagePullPolicy":"IfNotPresent",
"name":"nginx",
"ports":[
{
"containerPort":80
},
{
"containerPort":8080
}
]
},
{
"image":"ghost:latest",
"imagePullPolicy":"IfNotPresent",
"name":"ghost",
"ports":[
{
"containerPort":8080
}
]
}
]
}
}
}
}`)
compareJsonAsMap(t, expectedResult, doc)
}
func TestApplyOverlay_AddingAnchor(t *testing.T) {
overlayRaw := []byte(`{
"metadata": {
"name": "nginx-deployment",
"labels": {
"+(app)": "should-not-be-here",
"+(key1)": "value1"
}
}
}`)
resourceRaw := []byte(`{
"metadata": {
"name": "nginx-deployment",
"labels": {
"app": "nginx"
}
}
}`)
var resource, overlay interface{}
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, len(patches) != 0)
doc, err := ApplyPatches(resourceRaw, patches)
assert.NilError(t, err)
expectedResult := []byte(`{
"metadata":{
"labels":{
"app":"nginx",
"key1":"value1"
},
"name":"nginx-deployment"
}
}`)
compareJsonAsMap(t, expectedResult, doc)
}
func TestApplyOverlay_AddingAnchorInsideListElement(t *testing.T) {
overlayRaw := []byte(`
{
"spec": {
"template": {
"spec": {
"containers": [
{
"(image)": "*:latest",
"+(imagePullPolicy)": "IfNotPresent"
}
]
}
}
}
}`)
resourceRaw := []byte(`
{
"apiVersion":"apps/v1",
"kind":"Deployment",
"metadata":{
"name":"nginx-deployment",
"labels":{
"app":"nginx"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"nginx"
}
},
"template":{
"metadata":{
"labels":{
"app":"nginx"
}
},
"spec":{
"containers":[
{
"image":"nginx:latest"
},
{
"image":"ghost:latest",
"imagePullPolicy":"Always"
},
{
"image":"debian:10"
},
{
"image":"ubuntu:18.04",
"imagePullPolicy":"Always"
}
]
}
}
}
}`)
var resource, overlay interface{}
json.Unmarshal(resourceRaw, &resource)
json.Unmarshal(overlayRaw, &overlay)
patches, res := applyOverlay(resource, overlay, "/")
assert.NilError(t, res.ToError())
assert.Assert(t, len(patches) != 0)
doc, err := ApplyPatches(resourceRaw, patches)
assert.NilError(t, err)
expectedResult := []byte(`
{
"apiVersion":"apps/v1",
"kind":"Deployment",
"metadata":{
"name":"nginx-deployment",
"labels":{
"app":"nginx"
}
},
"spec":{
"replicas":1,
"selector":{
"matchLabels":{
"app":"nginx"
}
},
"template":{
"metadata":{
"labels":{
"app":"nginx"
}
},
"spec":{
"containers":[
{
"image":"nginx:latest",
"imagePullPolicy":"IfNotPresent"
},
{
"image":"ghost:latest",
"imagePullPolicy":"Always"
},
{
"image":"debian:10"
},
{
"image":"ubuntu:18.04",
"imagePullPolicy":"Always"
}
]
}
}
}
}`)
compareJsonAsMap(t, expectedResult, doc)
}

View file

@ -3,7 +3,6 @@ package engine
import (
"math"
"regexp"
"strconv"
"strings"
"github.com/golang/glog"
@ -58,6 +57,7 @@ func ValidateValueWithPattern(value, pattern interface{}) bool {
}
}
// Handler for int values during validation process
func validateValueWithIntPattern(value interface{}, pattern int64) bool {
switch typedValue := value.(type) {
case int:
@ -78,6 +78,7 @@ func validateValueWithIntPattern(value interface{}, pattern int64) bool {
}
}
// Handler for float values during validation process
func validateValueWithFloatPattern(value interface{}, pattern float64) bool {
switch typedValue := value.(type) {
case int:
@ -96,6 +97,7 @@ func validateValueWithFloatPattern(value interface{}, pattern float64) bool {
}
}
// Handler for nil values during validation process
func validateValueWithNilPattern(value interface{}) bool {
switch typed := value.(type) {
case float64:
@ -119,6 +121,7 @@ func validateValueWithNilPattern(value interface{}) bool {
}
}
// Handler for pattern values during validation process
func validateValueWithStringPatterns(value interface{}, pattern string) bool {
statements := strings.Split(pattern, "|")
for _, statement := range statements {
@ -131,6 +134,8 @@ func validateValueWithStringPatterns(value interface{}, pattern string) bool {
return false
}
// Handler for single pattern value during validation process
// Detects if pattern has a number
func validateValueWithStringPattern(value interface{}, pattern string) bool {
operator := getOperatorFromStringPattern(pattern)
pattern = pattern[len(operator):]
@ -143,6 +148,7 @@ func validateValueWithStringPattern(value interface{}, pattern string) bool {
return validateNumberWithStr(value, number, str, operator)
}
// Handler for string values
func validateString(value interface{}, pattern string, operator Operator) bool {
if NotEqual == operator || Equal == operator {
strValue, ok := value.(string)
@ -164,6 +170,7 @@ func validateString(value interface{}, pattern string, operator Operator) bool {
return false
}
// validateNumberWithStr applies wildcard to suffix and operator to numerical part
func validateNumberWithStr(value interface{}, patternNumber, patternStr string, operator Operator) bool {
// pattern has suffix
if "" != patternStr {
@ -179,51 +186,21 @@ func validateNumberWithStr(value interface{}, patternNumber, patternStr string,
return false
}
valueParsedNumber, err := parseNumber(valueNumber)
if err != nil {
return false
}
return validateNumber(valueParsedNumber, patternNumber, operator)
return validateNumber(valueNumber, patternNumber, operator)
}
return validateNumber(value, patternNumber, operator)
}
// validateNumber compares two numbers with operator
func validateNumber(value, pattern interface{}, operator Operator) bool {
var floatPattern, floatValue float64
switch typed := value.(type) {
case string:
var err error
floatValue, err = strconv.ParseFloat(typed, 64)
if err != nil {
return false
}
case float64:
floatValue = typed
case int64:
floatValue = float64(typed)
case int:
floatValue = float64(typed)
default:
floatPattern, err := convertToFloat(pattern)
if err != nil {
return false
}
switch typed := pattern.(type) {
case string:
var err error
floatPattern, err = strconv.ParseFloat(typed, 64)
if err != nil {
return false
}
case float64:
floatPattern = typed
case int64:
floatPattern = float64(typed)
case int:
floatPattern = float64(typed)
default:
floatValue, err := convertToFloat(value)
if err != nil {
return false
}
@ -245,6 +222,7 @@ func validateNumber(value, pattern interface{}, operator Operator) bool {
return false
}
// getOperatorFromStringPattern parses opeartor from pattern
func getOperatorFromStringPattern(pattern string) Operator {
if len(pattern) < 2 {
return Equal
@ -273,6 +251,7 @@ func getOperatorFromStringPattern(pattern string) Operator {
return Equal
}
// detects numerical and string parts in pattern and returns them
func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
regexpStr := `^(\d*(\.\d+)?)(.*)`
re := regexp.MustCompile(regexpStr)
@ -280,17 +259,3 @@ func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
match := matches[0]
return match[1], match[3]
}
func parseNumber(number string) (interface{}, error) {
var err error
if floatValue, err := strconv.ParseFloat(number, 64); err == nil {
return floatValue, nil
}
if intValue, err := strconv.ParseInt(number, 10, 64); err == nil {
return intValue, nil
}
return nil, err
}

View file

@ -2,6 +2,8 @@ package engine
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/minio/minio/pkg/wildcard"
@ -98,7 +100,7 @@ func ParseNamespaceFromObject(bytes []byte) string {
return ""
}
// returns true if policyResourceName is a regexp
// ParseRegexPolicyResourceName returns true if policyResourceName is a regexp
func ParseRegexPolicyResourceName(policyResourceName string) (string, bool) {
regex := strings.Split(policyResourceName, "regex:")
if len(regex) == 1 {
@ -111,7 +113,7 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{}
result := make(map[string]interface{})
for key, value := range anchorsMap {
if wrappedWithParentheses(key) {
if isConditionAnchor(key) || isExistanceAnchor(key) {
result[key] = value
}
}
@ -119,6 +121,16 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{}
return result
}
func getAnchorFromMap(anchorsMap map[string]interface{}) (string, interface{}) {
for key, value := range anchorsMap {
if isConditionAnchor(key) || isExistanceAnchor(key) {
return key, value
}
}
return "", nil
}
func findKind(kinds []string, kindGVK string) bool {
for _, kind := range kinds {
if kind == kindGVK {
@ -128,7 +140,7 @@ func findKind(kinds []string, kindGVK string) bool {
return false
}
func wrappedWithParentheses(str string) bool {
func isConditionAnchor(str string) bool {
if len(str) < 2 {
return false
}
@ -136,6 +148,28 @@ func wrappedWithParentheses(str string) bool {
return (str[0] == '(' && str[len(str)-1] == ')')
}
func isExistanceAnchor(str string) bool {
left := "^("
right := ")"
if len(str) < len(left)+len(right) {
return false
}
return (str[:len(left)] == left && str[len(str)-len(right):] == right)
}
func isAddingAnchor(key string) bool {
const left = "+("
const right = ")"
if len(key) < len(left)+len(right) {
return false
}
return left == key[:len(left)] && right == key[len(key)-len(right):]
}
// Checks if array object matches anchors. If not - skip - return true
func skipArrayObject(object, anchors map[string]interface{}) bool {
for key, pattern := range anchors {
@ -153,3 +187,38 @@ func skipArrayObject(object, anchors map[string]interface{}) bool {
return false
}
// removeAnchor remove special characters around anchored key
func removeAnchor(key string) string {
if isConditionAnchor(key) {
return key[1 : len(key)-1]
}
if isExistanceAnchor(key) || isAddingAnchor(key) {
return key[2 : len(key)-1]
}
return key
}
// convertToFloat converts string and any other value to float64
func convertToFloat(value interface{}) (float64, error) {
switch typed := value.(type) {
case string:
var err error
floatValue, err := strconv.ParseFloat(typed, 64)
if err != nil {
return 0, err
}
return floatValue, nil
case float64:
return typed, nil
case int64:
return float64(typed), nil
case int:
return float64(typed), nil
default:
return 0, fmt.Errorf("Could not convert %T to float64", value)
}
}

View file

@ -331,3 +331,66 @@ func TestResourceMeetsDescription_MatchLabelsAndMatchExpressions(t *testing.T) {
assert.Assert(t, false == ResourceMeetsDescription(rawResource, resourceDescription, groupVersionKind))
}
func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) {
str := "(something)"
assert.Assert(t, isConditionAnchor(str))
}
func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) {
str := "()"
assert.Assert(t, isConditionAnchor(str))
}
func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) {
str := "something"
assert.Assert(t, !isConditionAnchor(str))
}
func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) {
str := "(something"
assert.Assert(t, !isConditionAnchor(str))
}
func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) {
str := "something)"
assert.Assert(t, !isConditionAnchor(str))
}
func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) {
str := "so)m(et(hin)g"
assert.Assert(t, !isConditionAnchor(str))
}
func TestWrappedWithParentheses_Empty(t *testing.T) {
str := ""
assert.Assert(t, !isConditionAnchor(str))
}
func TestIsExistanceAnchor_Yes(t *testing.T) {
assert.Assert(t, isExistanceAnchor("^(abc)"))
}
func TestIsExistanceAnchor_NoRightBracket(t *testing.T) {
assert.Assert(t, !isExistanceAnchor("^(abc"))
}
func TestIsExistanceAnchor_OnlyHat(t *testing.T) {
assert.Assert(t, !isExistanceAnchor("^abc"))
}
func TestIsExistanceAnchor_ConditionAnchor(t *testing.T) {
assert.Assert(t, !isExistanceAnchor("(abc)"))
}
func TestRemoveAnchor_ConditionAnchor(t *testing.T) {
assert.Equal(t, removeAnchor("(abc)"), "abc")
}
func TestRemoveAnchor_ExistanceAnchor(t *testing.T) {
assert.Equal(t, removeAnchor("^(abc)"), "abc")
}
func TestRemoveAnchor_EmptyExistanceAnchor(t *testing.T) {
assert.Equal(t, removeAnchor("^()"), "")
}

View file

@ -33,8 +33,8 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
validationResult := validateResourceWithPattern(resource, rule.Validation.Pattern)
if result.Success != validationResult.Reason {
ruleApplicationResult.MergeWith(&validationResult)
ruleApplicationResult.AddMessagef(*rule.Validation.Message)
ruleApplicationResult.MergeWith(&validationResult)
} else {
ruleApplicationResult.AddMessagef("Success")
}
@ -45,68 +45,76 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
return policyResult
}
// validateResourceWithPattern is a start of element-by-element validation process
// It assumes that validation is started from root, so "/" is passed
func validateResourceWithPattern(resource, pattern interface{}) result.RuleApplicationResult {
return validateResourceElement(resource, pattern, "/")
}
func validateResourceElement(value, pattern interface{}, path string) result.RuleApplicationResult {
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)
// and calls corresponding handler
// Pattern tree and resource tree can have different structure. In this case validation fails
func validateResourceElement(resourceElement, patternElement interface{}, path string) result.RuleApplicationResult {
res := result.NewRuleApplicationResult("")
// TODO: Move similar message templates to message package
switch typedPattern := pattern.(type) {
switch typedPatternElement := patternElement.(type) {
// map
case map[string]interface{}:
typedValue, ok := value.(map[string]interface{})
typedResourceElement, ok := resourceElement.(map[string]interface{})
if !ok {
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value)
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement)
return res
}
return validateMap(typedValue, typedPattern, path)
return validateMap(typedResourceElement, typedPatternElement, path)
// array
case []interface{}:
typedValue, ok := value.([]interface{})
typedResourceElement, ok := resourceElement.([]interface{})
if !ok {
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value)
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement)
return res
}
return validateArray(typedValue, typedPattern, path)
return validateArray(typedResourceElement, typedPatternElement, path)
// elementary values
case string, float64, int, int64, bool, nil:
if !ValidateValueWithPattern(value, pattern) {
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
if !ValidateValueWithPattern(resourceElement, patternElement) {
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", resourceElement, patternElement, path)
}
return res
default:
res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", pattern, path)
res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", patternElement, path)
return res
}
}
func validateMap(valueMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
// If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap
// For each element of the map we must detect the type again, so we pass these elements to validateResourceElement
func validateMap(resourceMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
res := result.NewRuleApplicationResult("")
for key, pattern := range patternMap {
if wrappedWithParentheses(key) {
key = key[1 : len(key)-1]
}
for key, patternElement := range patternMap {
key = removeAnchor(key)
if pattern == "*" && valueMap[key] != nil {
// The '*' pattern means that key exists and has value
if patternElement == "*" && resourceMap[key] != nil {
continue
} else if pattern == "*" && valueMap[key] == nil {
} else if patternElement == "*" && resourceMap[key] == nil {
res.FailWithMessagef("Field %s is not present", key)
} else {
elementResult := validateResourceElement(valueMap[key], pattern, path+key+"/")
if result.Failed == elementResult.Reason {
res.Reason = elementResult.Reason
res.Messages = append(res.Messages, elementResult.Messages...)
}
elementResult := validateResourceElement(resourceMap[key], patternElement, path+key+"/")
res.MergeWith(&elementResult)
}
}
return res
}
// If validateResourceElement detects array element inside resource and pattern trees, it goes to validateArray
// Unlike the validateMap, we should check the array elements type on-site, because in case of maps, we should
// get anchors and check each array element with it.
func validateArray(resourceArray, patternArray []interface{}, path string) result.RuleApplicationResult {
res := result.NewRuleApplicationResult("")
@ -114,36 +122,29 @@ func validateArray(resourceArray, patternArray []interface{}, path string) resul
return res
}
switch pattern := patternArray[0].(type) {
switch typedPatternElement := patternArray[0].(type) {
case map[string]interface{}:
anchors := getAnchorsFromMap(pattern)
for i, value := range resourceArray {
currentPath := path + strconv.Itoa(i) + "/"
resource, ok := value.(map[string]interface{})
if !ok {
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, pattern, value)
return res
}
if skipArrayObject(resource, anchors) {
continue
}
mapValidationResult := validateMap(resource, pattern, currentPath)
if result.Failed == mapValidationResult.Reason {
res.Reason = mapValidationResult.Reason
res.Messages = append(res.Messages, mapValidationResult.Messages...)
}
}
case string, float64, int, int64, bool, nil:
for _, value := range resourceArray {
if !ValidateValueWithPattern(value, pattern) {
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
}
}
// This is special case, because maps in arrays can have anchors that must be
// processed with the special way affecting the entire array
arrayResult := validateArrayOfMaps(resourceArray, typedPatternElement, path)
res.MergeWith(&arrayResult)
default:
res.FailWithMessagef("Array element pattern of unknown type %T. Path: %s", pattern, path)
// In all other cases - detect type and handle each array element with validateResourceElement
for i, patternElement := range patternArray {
currentPath := path + strconv.Itoa(i) + "/"
elementResult := validateResourceElement(resourceArray[i], patternElement, currentPath)
res.MergeWith(&elementResult)
}
}
return res
}
// validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic
// and then validates each map due to the pattern
func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
anchor, pattern := getAnchorFromMap(patternMap)
handler := CreateAnchorHandler(anchor, pattern, path)
return handler.Handle(resourceMapArray, patternMap)
}

View file

@ -9,41 +9,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) {
str := "(something)"
assert.Assert(t, wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) {
str := "()"
assert.Assert(t, wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) {
str := "something"
assert.Assert(t, !wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) {
str := "(something"
assert.Assert(t, !wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) {
str := "something)"
assert.Assert(t, !wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) {
str := "so)m(et(hin)g"
assert.Assert(t, !wrappedWithParentheses(str))
}
func TestWrappedWithParentheses_Empty(t *testing.T) {
str := ""
assert.Assert(t, !wrappedWithParentheses(str))
}
func TestValidateString_AsteriskTest(t *testing.T) {
pattern := "*"
value := "anything"

View file

@ -82,7 +82,7 @@ func (b *builder) processViolation(info Info) error {
modifiedPolicy.Status.Violations = modifiedViolations
// Violations are part of the status sub resource, so we can use the Update Status api instead of updating the policy object
_, err = b.client.UpdateStatusResource("policies/status", namespace, modifiedPolicy)
_, err = b.client.UpdateStatusResource("policies/status", namespace, modifiedPolicy, false)
if err != nil {
return err
}

View file

@ -2,8 +2,10 @@ package webhooks
import (
"errors"
"fmt"
"io/ioutil"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/config"
client "github.com/nirmata/kyverno/pkg/dclient"
@ -18,10 +20,12 @@ type WebhookRegistrationClient struct {
registrationClient *admregclient.AdmissionregistrationV1beta1Client
client *client.Client
clientConfig *rest.Config
// serverIP should be used if running Kyverno out of clutser
serverIP string
}
// NewWebhookRegistrationClient creates new WebhookRegistrationClient instance
func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Client) (*WebhookRegistrationClient, error) {
func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Client, serverIP string) (*WebhookRegistrationClient, error) {
registrationClient, err := admregclient.NewForConfig(clientConfig)
if err != nil {
return nil, err
@ -31,11 +35,15 @@ func NewWebhookRegistrationClient(clientConfig *rest.Config, client *client.Clie
registrationClient: registrationClient,
client: client,
clientConfig: clientConfig,
serverIP: serverIP,
}, nil
}
// Register creates admission webhooks configs on cluster
func (wrc *WebhookRegistrationClient) Register() error {
if wrc.serverIP != "" {
glog.Infof("Registering webhook with url https://%s\n", wrc.serverIP)
}
// For the case if cluster already has this configs
wrc.Deregister()
@ -66,6 +74,12 @@ func (wrc *WebhookRegistrationClient) Register() error {
// This function does not fail on error:
// Register will fail if the config exists, so there is no need to fail on error
func (wrc *WebhookRegistrationClient) Deregister() {
if wrc.serverIP != "" {
wrc.registrationClient.MutatingWebhookConfigurations().Delete(config.MutatingWebhookConfigurationDebug, &meta.DeleteOptions{})
wrc.registrationClient.ValidatingWebhookConfigurations().Delete(config.ValidatingWebhookConfigurationDebug, &meta.DeleteOptions{})
return
}
wrc.registrationClient.MutatingWebhookConfigurations().Delete(config.MutatingWebhookConfigurationName, &meta.DeleteOptions{})
wrc.registrationClient.ValidatingWebhookConfigurations().Delete(config.ValidatingWebhookConfigurationName, &meta.DeleteOptions{})
}
@ -83,6 +97,10 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati
return nil, errors.New("Unable to extract CA data from configuration")
}
if wrc.serverIP != "" {
return wrc.contructDebugMutatingWebhookConfig(caData), nil
}
return &admregapi.MutatingWebhookConfiguration{
ObjectMeta: meta.ObjectMeta{
Name: config.MutatingWebhookConfigurationName,
@ -100,6 +118,24 @@ func (wrc *WebhookRegistrationClient) constructMutatingWebhookConfig(configurati
}, nil
}
func (wrc *WebhookRegistrationClient) contructDebugMutatingWebhookConfig(caData []byte) *admregapi.MutatingWebhookConfiguration {
url := fmt.Sprintf("https://%s%s", wrc.serverIP, config.MutatingWebhookServicePath)
glog.V(3).Infof("Debug MutatingWebhookConfig is registered with url %s\n", url)
return &admregapi.MutatingWebhookConfiguration{
ObjectMeta: meta.ObjectMeta{
Name: config.MutatingWebhookConfigurationDebug,
Labels: config.KubePolicyAppLabels,
},
Webhooks: []admregapi.Webhook{
constructDebugWebhook(
config.MutatingWebhookName,
url,
caData),
},
}
}
func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configuration *rest.Config) (*admregapi.ValidatingWebhookConfiguration, error) {
// Check if ca is defined in the secret tls-ca
// assume the key and signed cert have been defined in secret tls.kyverno
@ -112,6 +148,10 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura
return nil, errors.New("Unable to extract CA data from configuration")
}
if wrc.serverIP != "" {
return wrc.contructDebugValidatingWebhookConfig(caData), nil
}
return &admregapi.ValidatingWebhookConfiguration{
ObjectMeta: meta.ObjectMeta{
Name: config.ValidatingWebhookConfigurationName,
@ -129,6 +169,24 @@ func (wrc *WebhookRegistrationClient) constructValidatingWebhookConfig(configura
}, nil
}
func (wrc *WebhookRegistrationClient) contructDebugValidatingWebhookConfig(caData []byte) *admregapi.ValidatingWebhookConfiguration {
url := fmt.Sprintf("https://%s%s", wrc.serverIP, config.ValidatingWebhookServicePath)
glog.V(3).Infof("Debug ValidatingWebhookConfig is registered with url %s\n", url)
return &admregapi.ValidatingWebhookConfiguration{
ObjectMeta: meta.ObjectMeta{
Name: config.ValidatingWebhookConfigurationName,
Labels: config.KubePolicyAppLabels,
},
Webhooks: []admregapi.Webhook{
constructDebugWebhook(
config.ValidatingWebhookName,
url,
caData),
},
}
}
func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook {
return admregapi.Webhook{
Name: name,
@ -161,6 +219,34 @@ func constructWebhook(name, servicePath string, caData []byte) admregapi.Webhook
}
}
func constructDebugWebhook(name, url string, caData []byte) admregapi.Webhook {
return admregapi.Webhook{
Name: name,
ClientConfig: admregapi.WebhookClientConfig{
URL: &url,
CABundle: caData,
},
Rules: []admregapi.RuleWithOperations{
admregapi.RuleWithOperations{
Operations: []admregapi.OperationType{
admregapi.Create,
},
Rule: admregapi.Rule{
APIGroups: []string{
"*",
},
APIVersions: []string{
"*",
},
Resources: []string{
"*/*",
},
},
},
},
}
}
func (wrc *WebhookRegistrationClient) constructOwner() meta.OwnerReference {
kubePolicyDeployment, err := wrc.client.GetKubePolicyDeployment()

View file

@ -29,6 +29,7 @@ type WebhookServer struct {
server http.Server
client *client.Client
policyLister v1alpha1.PolicyLister
filterKinds []string
}
// NewWebhookServer creates new instance of WebhookServer accordingly to given configuration
@ -36,7 +37,8 @@ type WebhookServer struct {
func NewWebhookServer(
client *client.Client,
tlsPair *tlsutils.TlsPemPair,
shareInformer sharedinformer.PolicyInformer) (*WebhookServer, error) {
shareInformer sharedinformer.PolicyInformer,
filterKinds []string) (*WebhookServer, error) {
if tlsPair == nil {
return nil, errors.New("NewWebhookServer is not initialized properly")
@ -52,8 +54,8 @@ func NewWebhookServer(
ws := &WebhookServer{
client: client,
policyLister: shareInformer.GetLister(),
filterKinds: parseKinds(filterKinds),
}
mux := http.NewServeMux()
mux.HandleFunc(config.MutatingWebhookServicePath, ws.serve)
mux.HandleFunc(config.ValidatingWebhookServicePath, ws.serve)
@ -79,11 +81,15 @@ func (ws *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
admissionReview.Response = &v1beta1.AdmissionResponse{
Allowed: true,
}
switch r.URL.Path {
case config.MutatingWebhookServicePath:
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
case config.ValidatingWebhookServicePath:
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
// Do not process the admission requests for kinds that are in filterKinds for filtering
if !StringInSlice(admissionReview.Request.Kind.Kind, ws.filterKinds) {
switch r.URL.Path {
case config.MutatingWebhookServicePath:
admissionReview.Response = ws.HandleMutation(admissionReview.Request)
case config.ValidatingWebhookServicePath:
admissionReview.Response = ws.HandleValidation(admissionReview.Request)
}
}
admissionReview.Response.UID = admissionReview.Request.UID
@ -124,8 +130,6 @@ func (ws *WebhookServer) Stop() {
// HandleMutation handles mutating webhook admission request
func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
glog.Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
@ -137,6 +141,14 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be
var allPatches []engine.PatchBytes
for _, policy := range policies {
// check if policy has a rule for the admission request kind
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
glog.V(3).Infof("Handling mutation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
glog.Infof("Applying policy %s with %d rules\n", policy.ObjectMeta.Name, len(policy.Spec.Rules))
policyPatches, mutationResult := engine.Mutate(*policy, request.Object.Raw, request.Kind)
@ -152,10 +164,10 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be
name := engine.ParseNameFromObject(request.Object.Raw)
glog.Infof("Mutation from policy %s has applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name)
}
glog.Info(admissionResult.String())
}
message := "\n" + admissionResult.String()
glog.Info(message)
if admissionResult.GetReason() == result.Success {
patchType := v1beta1.PatchTypeJSONPatch
@ -176,8 +188,6 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be
// HandleValidation handles validating webhook admission request
func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1beta1.AdmissionResponse {
glog.Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
policies, err := ws.policyLister.List(labels.NewSelector())
if err != nil {
@ -187,6 +197,14 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1
admissionResult := result.NewAdmissionResult(string(request.UID))
for _, policy := range policies {
if !StringInSlice(request.Kind.Kind, getApplicableKindsForPolicy(policy)) {
continue
}
glog.V(3).Infof("Handling validation for Kind=%s, Namespace=%s Name=%s UID=%s patchOperation=%s",
request.Kind.Kind, request.Namespace, request.Name, request.UID, request.Operation)
glog.Infof("Validating resource with policy %s with %d rules", policy.ObjectMeta.Name, len(policy.Spec.Rules))
validationResult := engine.Validate(*policy, request.Object.Raw, request.Kind)
admissionResult = result.Append(admissionResult, validationResult)
@ -194,10 +212,10 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1
if validationError := validationResult.ToError(); validationError != nil {
glog.Warningf(validationError.Error())
}
glog.Info(admissionResult.String())
}
message := "\n" + admissionResult.String()
glog.Info(message)
// Generation loop after all validation succeeded
var response *v1beta1.AdmissionResponse
@ -206,7 +224,7 @@ func (ws *WebhookServer) HandleValidation(request *v1beta1.AdmissionRequest) *v1
for _, policy := range policies {
engine.Generate(ws.client, *policy, request.Object.Raw, request.Kind)
}
glog.Info("Validation is successful")
glog.V(3).Info("Validation is successful")
response = &v1beta1.AdmissionResponse{
Allowed: true,

65
pkg/webhooks/utils.go Normal file
View file

@ -0,0 +1,65 @@
package webhooks
import (
"strings"
"github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
)
//StringInSlice checks if string is present in slice of strings
func StringInSlice(kind string, list []string) bool {
for _, b := range list {
if b == kind {
return true
}
}
return false
}
//parseKinds parses the kinds if a single string contains comma seperated kinds
// {"1,2,3","4","5"} => {"1","2","3","4","5"}
func parseKinds(list []string) []string {
kinds := []string{}
for _, k := range list {
args := strings.Split(k, ",")
for _, arg := range args {
if arg != "" {
kinds = append(kinds, strings.TrimSpace(arg))
}
}
}
return kinds
}
type ArrayFlags []string
func (i *ArrayFlags) String() string {
var sb strings.Builder
for _, str := range *i {
sb.WriteString(str)
}
return sb.String()
}
func (i *ArrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
// extract the kinds that the policy rules apply to
func getApplicableKindsForPolicy(p *v1alpha1.Policy) []string {
kindsMap := map[string]interface{}{}
kinds := []string{}
// iterate over the rules an identify all kinds
for _, rule := range p.Spec.Rules {
for _, k := range rule.ResourceDescription.Kinds {
kindsMap[k] = nil
}
}
// get the kinds
for k := range kindsMap {
kinds = append(kinds, k)
}
return kinds
}

2
scripts/cleanup.sh Executable file
View file

@ -0,0 +1,2 @@
kubectl delete -f definitions/install.yaml
kubectl delete csr,MutatingWebhookConfiguration,ValidatingWebhookConfiguration --all

View file

@ -0,0 +1,36 @@
#!/bin/bash
for i in "$@"
do
case $i in
--service=*)
service="${i#*=}"
shift
;;
--serverIP=*)
serverIP="${i#*=}"
shift
;;
esac
done
if [ -z "${serverIP}" ]; then
echo -e "Please specify '--serverIP' where Kyverno controller runs."
exit 1
fi
if [ -z "${service}" ]; then
service="localhost"
fi
echo "service is $service"
echo "serverIP is $serverIP"
echo "Generating certificate for the service ${service}..."
certsGenerator="./scripts/generate-self-signed-cert-and-k8secrets-debug.sh"
chmod +x "${certsGenerator}"
${certsGenerator} "--service=${service}" "--serverIP=${serverIP}" || exit 2
echo -e "\n### You can build and run kyverno project locally.\n### To check its work, run it with flags --kubeconfig and --serverIP parameters."

View file

@ -0,0 +1,71 @@
#!/bin/bash
for i in "$@"
do
case $i in
--service=*)
service="${i#*=}"
shift
;;
--serverIP=*)
serverIP="${i#*=}"
shift
;;
esac
done
destdir="certs"
if [ ! -d "$destdir" ]; then
mkdir ${destdir} || exit 1
fi
tmpdir=$(mktemp -d)
cat <<EOF >> ${tmpdir}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${service}
IP.1 = ${serverIP}
EOF
if [ ! -z "${service}" ]; then
subjectCN="${service}"
else
subjectCN=${serverIP}
fi
echo "Generating self-signed certificate for CN=${subjectCN}"
# generate priv key for root CA
openssl genrsa -out ${destdir}/rootCA.key 4096
# generate root CA
openssl req -x509 -new -nodes -key ${destdir}/rootCA.key -sha256 -days 1024 -out ${destdir}/rootCA.crt -subj "/CN=${subjectCN}"
# generate priv key
openssl genrsa -out ${destdir}/webhook.key 4096
# generate certificate
openssl req -new -key ${destdir}/webhook.key -out ${destdir}/webhook.csr -subj "/CN=${subjectCN}" -config ${tmpdir}/csr.conf
# sign the certificate using the root CA
openssl x509 -req -in ${destdir}/webhook.csr -CA ${destdir}/rootCA.crt -CAkey ${destdir}/rootCA.key -CAcreateserial -out ${destdir}/webhook.crt -days 1024 -sha256 -extensions v3_req -extfile ${tmpdir}/csr.conf
kubectl delete -f definitions/install_debug.yaml 2>/dev/null
kubectl delete csr,MutatingWebhookConfiguration,ValidatingWebhookConfiguration --all 2>/dev/null
echo "Generating corresponding kubernetes secrets for TLS pair and root CA"
# create project namespace
kubectl create ns kyverno
# create tls pair secret
kubectl -n kyverno create secret tls kyverno-svc.kyverno.svc.kyverno-tls-pair --cert=${destdir}/webhook.crt --key=${destdir}/webhook.key
# annotate tls pair secret to specify use of self-signed certificates and check if root CA is created as secret
kubectl annotate secret kyverno-svc.kyverno.svc.kyverno-tls-pair -n kyverno self-signed-cert=true
# create root CA secret
kubectl -n kyverno create secret generic kyverno-svc.kyverno.svc.kyverno-tls-ca --from-file=${destdir}/rootCA.crt
echo "Creating CRD"
kubectl apply -f definitions/install_debug.yaml