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:
commit
e1d4effd4e
34 changed files with 1629 additions and 531 deletions
39
CODE_OF_CONDUCT.md
Normal file
39
CODE_OF_CONDUCT.md
Normal 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/
|
29
README.md
29
README.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ spec:
|
|||
containers:
|
||||
- name: kyverno
|
||||
image: nirmata/kyverno:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args: ["--filterKind","Nodes,Events,APIService,SubjectAccessReview"]
|
||||
ports:
|
||||
- containerPort: 443
|
||||
securityContext:
|
||||
|
|
126
definitions/install_debug.yaml
Normal file
126
definitions/install_debug.yaml
Normal 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"
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
...
|
||||
````
|
||||
|
|
|
@ -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
10
init.go
|
@ -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
11
main.go
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
160
pkg/engine/anchor.go
Normal 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
|
||||
}
|
|
@ -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{}:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("^()"), "")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
65
pkg/webhooks/utils.go
Normal 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
2
scripts/cleanup.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
kubectl delete -f definitions/install.yaml
|
||||
kubectl delete csr,MutatingWebhookConfiguration,ValidatingWebhookConfiguration --all
|
36
scripts/deploy-controller-debug.sh
Executable file
36
scripts/deploy-controller-debug.sh
Executable 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."
|
||||
|
71
scripts/generate-self-signed-cert-and-k8secrets-debug.sh
Executable file
71
scripts/generate-self-signed-cert-and-k8secrets-debug.sh
Executable 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
|
Loading…
Reference in a new issue