1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

527 resolved merge conflicts

This commit is contained in:
shravan 2020-02-24 20:19:28 +05:30
commit 36e775edb0
69 changed files with 99030 additions and 3080 deletions

View file

@ -73,9 +73,9 @@ docker-push-kyverno:
##################################
# CLI
##################################
CLI_PATH := cmd/cli
CLI_PATH := cmd/cli/kubectl-kyverno
cli:
GOOS=$(GOOS) go build -o $(PWD)/$(CLI_PATH)/kyvernocli -ldflags=$(LD_FLAGS) $(PWD)/$(CLI_PATH)/main.go
GOOS=$(GOOS) go build -o $(PWD)/$(CLI_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(CLI_PATH)/main.go
##################################

View file

@ -129,6 +129,7 @@ Refer to a list of curated of ***[sample policies](/samples/README.md)*** that c
* [Background Processing](documentation/writing-policies-background.md)
* [Testing Policies](documentation/testing-policies.md)
* [Policy Violations](documentation/policy-violations.md)
* [Kyverno CLI](documentation/kyverno-cli.md)
* [Sample Policies](/samples/README.md)
## License

View file

@ -1,25 +0,0 @@
package main
import (
goflag "flag"
"fmt"
"os"
"github.com/nirmata/kyverno/pkg/config"
kyverno "github.com/nirmata/kyverno/pkg/kyverno"
flag "github.com/spf13/pflag"
)
func main() {
cmd := kyverno.NewDefaultKyvernoCommand()
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
func init() {
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
config.LogDefaultFlags()
flag.Parse()
}

View file

@ -6,6 +6,8 @@ package main
import (
"flag"
"os"
"regexp"
"strconv"
"sync"
"time"
@ -44,6 +46,11 @@ func main() {
glog.Fatalf("Error creating client: %v\n", err)
}
// Exit for unsupported version of kubernetes cluster
// https://github.com/nirmata/kyverno/issues/700
// - supported from v1.12.7+
isVersionSupported(client)
requests := []request{
// Resource
{validatingWebhookConfigKind, config.ValidatingWebhookConfigurationName},
@ -206,3 +213,32 @@ func merge(done <-chan struct{}, stopCh <-chan struct{}, processes ...<-chan err
}()
return out
}
func isVersionSupported(client *client.Client) {
serverVersion, err := client.DiscoveryClient.GetServerVersion()
if err != nil {
glog.Fatalf("Failed to get kubernetes server version: %v\n", err)
}
exp := regexp.MustCompile(`v(\d*).(\d*).(\d*)`)
groups := exp.FindAllStringSubmatch(serverVersion.String(), -1)
if len(groups) != 1 || len(groups[0]) != 4 {
glog.Fatalf("Failed to extract kubernetes server version: %v.err %v\n", serverVersion, err)
}
// convert string to int
// assuming the version are always intergers
major, err := strconv.Atoi(groups[0][1])
if err != nil {
glog.Fatalf("Failed to extract kubernetes major server version: %v.err %v\n", serverVersion, err)
}
minor, err := strconv.Atoi(groups[0][2])
if err != nil {
glog.Fatalf("Failed to extract kubernetes minor server version: %v.err %v\n", serverVersion, err)
}
sub, err := strconv.Atoi(groups[0][3])
if err != nil {
glog.Fatalf("Failed to extract kubernetes sub minor server version:%v. err %v\n", serverVersion, err)
}
if major <= 1 && minor <= 12 && sub < 7 {
glog.Fatalf("Unsupported kubernetes server version %s. Kyverno is supported from version v1.12.7+", serverVersion)
}
}

97413
data/swaggerDoc.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,8 @@ spec:
enum:
- enforce # blocks the resorce api-reques if a rule fails.
- audit # allows resource creation and reports the failed validation rules as violations. Default
background:
type: boolean
rules:
type: array
items:
@ -468,27 +470,179 @@ metadata:
namespace: kyverno
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kyverno-admin
name: kyverno:webhook
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
name: kyverno:webhook
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kyverno:userinfo
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:userinfo
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kyverno:customresources
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:customresources
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kyverno:policycontroller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:policycontroller
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kyverno:generatecontroller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:generatecontroller
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: policyviolation
name: kyverno:webhook
rules:
- apiGroups: ["kyverno.io"]
# Dynamic creation of webhooks, events & certs
- apiGroups:
- '*'
resources:
- events
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
- certificatesigningrequests
- certificatesigningrequests/approval
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:userinfo
rules:
# get the roleRef for incoming api-request user
- apiGroups:
- "*"
resources:
- rolebindings
- clusterrolebindings
- configmaps
verbs:
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:customresources
rules:
# Kyverno CRs
- apiGroups:
- '*'
resources:
- clusterpolicies
- clusterpolicies/status
- clusterpolicyviolations
- clusterpolicyviolations/status
- policyviolations
verbs: ["get", "list", "watch"]
- policyviolations/status
- generaterequests
- generaterequests/status
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:policycontroller
rules:
# background processing, identify all existing resources
- apiGroups:
- '*'
resources:
- '*'
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:generatecontroller
rules:
# process generate rules to generate resources
- apiGroups:
- "*"
resources:
- namespaces
- networkpolicies
- secrets
- configmaps
- resourcequotas
- limitranges
- clusterroles
- rolebindings
- clusterrolebindings
verbs:
- create
- update
- delete
- get
# dynamic watches on trigger resources for generate rules
# re-evaluate the policy if the resource is updated
- apiGroups:
- '*'
resources:
- namespaces
verbs:
- watch
---
apiVersion: v1
kind: ConfigMap
@ -519,10 +673,10 @@ spec:
serviceAccountName: kyverno-service-account
initContainers:
- name: kyverno-pre
image: nirmata/kyvernopre:v1.1.2
image: nirmata/kyvernopre:v1.1.3
containers:
- name: kyverno
image: nirmata/kyverno:v1.1.2
image: nirmata/kyverno:v1.1.3
args:
- "--filterK8Resources=[Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][*,kyverno,*]"
# customize webhook timout

View file

@ -30,6 +30,8 @@ spec:
enum:
- enforce # blocks the resorce api-reques if a rule fails.
- audit # allows resource creation and reports the failed validation rules as violations. Default
background:
type: boolean
rules:
type: array
items:

View file

@ -81,7 +81,57 @@ Secret | Data | Content
Kyverno uses secrets created above to setup TLS communication with the kube-apiserver and specify the CA bundle to be used to validate the webhook server's certificate in the admission webhook configurations.
### 3. Install Kyverno
### 3. Configure Kyverno Role
Kyverno, in `foreground` mode, leverages admission webhooks to manage incoming api-requests, and `background` mode applies the policies on existing resources. It uses ServiceAccount `kyverno-service-account`, which is bound to multiple ClusterRole, which defines the default resources and operations that are permitted.
ClusterRoles used by kyverno:
- kyverno:webhook
- kyverno:userinfo
- kyverno:customresources
- kyverno:policycontroller
- kyverno:generatecontroller
The `generate` rule creates a new resource, and to allow kyverno to create resource kyverno ClusterRole needs permissions to create/update/delete. This can be done by adding the resource to the ClusterRole `kyverno:generatecontroller` used by kyverno or by creating a new ClusterRole and a ClusterRoleBinding to kyverno's default ServiceAccount.
```yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: kyverno:generatecontroller
rules:
- apiGroups:
- "*"
resources:
- namespaces
- networkpolicies
- secrets
- configmaps
- resourcequotas
- limitranges
- ResourceA # new Resource to be generated
- ResourceB
verbs:
- create # generate new resources
- get # check the contents of exiting resources
- update # update existing resource, if required configuration defined in policy is not present
- delete # clean-up, if the generate trigger resource is deleted
```
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: kyverno-admin-generate
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:generatecontroller # clusterRole defined above, to manage generated resources
subjects:
- kind: ServiceAccount
name: kyverno-service-account # default kyverno serviceAccount
namespace: kyverno
```
### 4. Install Kyverno
To install a specific version, change the image tag with git tag in `install.yaml`.

View file

@ -0,0 +1,60 @@
<small>*[documentation](/README.md#documentation) / kyverno-cli*</small>
# Kyverno CLI
The Kyverno Command Line Interface (CLI) is designed to validate policies and test the behavior of applying policies to resources before adding the policy to a cluster. It can be used as a kubectl plugin and as a standalone CLI.
## Build the CLI
You can build the CLI binary locally, then move the binary into a directory in your PATH.
```bash
git clone https://github.com/nirmata/kyverno.git
cd github.com/nirmata/kyverno
make cli
mv ./cmd/cli/kubectl-kyverno/kyverno /usr/local/bin/kyverno
```
## Commands
#### Version
Prints the version of kyverno used by the CLI.
Example:
```
kyverno version
```
#### Validate
Validates a policy, can validate multiple policy resource description files or even an entire folder containing policy resource description
files. Currently supports files with resource description in yaml.
Example:
```
kyverno validate /path/to/policy1.yaml /path/to/policy2.yaml /path/to/folderFullOfPolicies
```
#### Apply
Applies policies on resources, and supports applying multiple policies on multiple resources in a single command.
Also supports applying the given policies to an entire cluster. The current kubectl context will be used to access the cluster.
Will return results to stdout.
Apply to a resource:
```
kyverno apply /path/to/policy.yaml --resource /path/to/resource.yaml
```
Apply to all matching resources in a cluster:
```
kyverno apply /path/to/policy.yaml --cluster > policy-results.txt
```
Apply multiple policies to multiple resources:
```
kyverno apply /path/to/policy1.yaml /path/to/folderFullOfPolicies --resource /path/to/resource1.yaml --resource /path/to/resource2.yaml --cluster
```
<small>*Read Next >> [Sample Policies](/samples/README.md)*</small>

View file

@ -26,3 +26,6 @@ docker validation-example2-gzfdf validation-example2 Deployment com
# Cluster Policy Violations
Cluster Policy Violations are like Policy Violations but created for cluster-wide resources.
<small>*Read Next >> [Kyverno CLI](/documentation/kyverno-cli.md)*</small>

View file

@ -22,4 +22,9 @@ Then compare the original resource definition in CM.yaml with the actual one:
kubectl get -f CM.yaml -o yaml
````
## Test using Kyverno CLI
The Kyverno CLI allows testing policies before they are applied to a cluster. It is documented at [Kyverno CLI](kyverno-cli.md)
<small>*Read Next >> [Policy Violations](/documentation/policy-violations.md)*</small>

10
go.mod
View file

@ -8,15 +8,17 @@ require (
github.com/gogo/protobuf v1.3.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 // indirect
github.com/googleapis/gnostic v0.3.1 // indirect
github.com/googleapis/gnostic v0.3.1
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/imdario/mergo v0.3.8 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
github.com/json-iterator/go v1.1.9 // indirect
github.com/minio/minio v0.0.0-20200114012931-30922148fbb5
github.com/ory/go-acc v0.1.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/spf13/pflag v1.0.5 // indirect
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
@ -29,10 +31,12 @@ require (
gotest.tools v2.2.0+incompatible
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
k8s.io/cli-runtime v0.0.0-20191004110135-b9eb767d2e1a
k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible
k8s.io/klog v1.0.0 // indirect
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a // indirect
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a
k8s.io/utils v0.0.0-20200109141947-94aeca20bf09 // indirect
sigs.k8s.io/kustomize v2.0.3+incompatible // indirect
)
// Added for go1.13 migration https://github.com/golang/go/issues/32805

19
go.sum
View file

@ -24,7 +24,9 @@ github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcy
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0 h1:0GoNN3taZV6QI81IXgCbxMyEaJDXMSIjArYBCYzVVvs=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2 h1:JCHLVE3B+kJde7bIEo5N4J+ZbLhp0J1Fs+ulyRws4gE=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/sarama v1.24.1/go.mod h1:fGP8eQ6PugKEI0iUETYYtnP6d1pH/bdDMTel1X5ajsU=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
@ -95,6 +97,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elazarl/goproxy v0.0.0-20181003060214-f58a169a71a5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@ -108,6 +111,7 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -116,9 +120,13 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1 h1:wSt/4CYxs70xbATrGXhokKF1i0tZjENLOo1ioIO13zk=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9 h1:tF+augKRWlWx0J0B7ZyyKSiTyV6E1zZe+7b3qQlcEf8=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501 h1:C1JKChikHGpXwT5UQDFaryIpDtyyGL/CR6C2kB7F1oc=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87 h1:zP3nY8Tk2E6RTkqGYrarZXuzh+ffyLDljLxCy1iJw80=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@ -316,7 +324,9 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -351,6 +361,8 @@ github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@ -443,6 +455,7 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5 h1:0x4qcEHDpruK6ML/m/YSlFUUu0UpRD3I2PHsNCuGnyA=
github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM=
@ -566,6 +579,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -950,6 +965,8 @@ k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b h1:aBGgKJUM9Hk/3AE8WaZIApnTxG35kbu
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d h1:Jmdtdt1ZnoGfWWIIik61Z7nKYgO3J+swQJtPYsP9wHA=
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/cli-runtime v0.0.0-20191004110135-b9eb767d2e1a h1:REMzGxu+NpG9dPRsE9my/fw9iYIecz1S8UFFl6hbe18=
k8s.io/cli-runtime v0.0.0-20191004110135-b9eb767d2e1a/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM=
k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible h1:bK03DJulJi9j05gwnXUufcs2j7h4M85YFvJ0dIlQ9k4=
k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
@ -962,6 +979,8 @@ k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKf
k8s.io/utils v0.0.0-20200109141947-94aeca20bf09 h1:sz6xjn8QP74104YNmJpzLbJ+a3ZtHt0tkD0g8vpdWNw=
k8s.io/utils v0.0.0-20200109141947-94aeca20bf09/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View file

@ -216,8 +216,8 @@ type Validation struct {
// Generation describes which resources will be created when other resource is created
type Generation struct {
ResourceSpec
Data interface{} `json:"data"`
Clone CloneFrom `json:"clone"`
Data interface{} `json:"data,omitempty"`
Clone CloneFrom `json:"clone,omitempty"`
}
// CloneFrom - location of the resource

View file

@ -97,6 +97,6 @@ func CreateClientConfig(kubeconfig string) (*rest.Config, error) {
glog.Info("Using in-cluster configuration")
return rest.InClusterConfig()
}
glog.Infof("Using configuration from '%s'", kubeconfig)
glog.V(4).Infof("Using configuration from '%s'", kubeconfig)
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}

View file

@ -16,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
patchTypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/dynamic"
@ -213,6 +214,7 @@ func convertToCSR(obj *unstructured.Unstructured) (*certificates.CertificateSign
//IDiscovery provides interface to mange Kind and GVR mapping
type IDiscovery interface {
GetGVRFromKind(kind string) schema.GroupVersionResource
GetServerVersion() (*version.Info, error)
}
// SetDiscovery sets the discovery client implementation
@ -265,6 +267,11 @@ func (c ServerPreferredResources) GetGVRFromKind(kind string) schema.GroupVersio
return gvr
}
//GetServerVersion returns the server version of the cluster
func (c ServerPreferredResources) GetServerVersion() (*version.Info, error) {
return c.cachedClient.ServerVersion()
}
func loadServerResources(k string, cdi discovery.CachedDiscoveryInterface) (schema.GroupVersionResource, error) {
serverresources, err := cdi.ServerPreferredResources()
emptyGVR := schema.GroupVersionResource{}

View file

@ -6,6 +6,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/dynamic/fake"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
)
@ -64,6 +65,10 @@ func (c *fakeDiscoveryClient) getGVR(resource string) schema.GroupVersionResourc
return schema.GroupVersionResource{}
}
func (c *fakeDiscoveryClient) GetServerVersion() (*version.Info, error) {
return nil, nil
}
func (c *fakeDiscoveryClient) GetGVRFromKind(kind string) schema.GroupVersionResource {
resource := strings.ToLower(kind) + "s"
return c.getGVR(resource)

View file

@ -30,16 +30,18 @@ type EvalInterface interface {
//Context stores the data resources as JSON
type Context struct {
mu sync.RWMutex
// data map[string]interface{}
jsonRaw []byte
mu sync.RWMutex
jsonRaw []byte
whiteListVars []string
}
//NewContext returns a new context
func NewContext() *Context {
// pass the list of variables to be white-listed
func NewContext(whiteListVars ...string) *Context {
ctx := Context{
// data: map[string]interface{}{},
jsonRaw: []byte(`{}`), // empty json struct
jsonRaw: []byte(`{}`), // empty json struct
whiteListVars: whiteListVars,
}
return &ctx
}
@ -122,7 +124,6 @@ func (ctx *Context) AddSA(userName string) error {
saNamespace = groups[0]
}
glog.V(4).Infof("Loading variable serviceAccountName with value: %s", saName)
saNameObj := struct {
SA string `json:"serviceAccountName"`
}{
@ -137,7 +138,6 @@ func (ctx *Context) AddSA(userName string) error {
return err
}
glog.V(4).Infof("Loading variable serviceAccountNamespace with value: %s", saNamespace)
saNsObj := struct {
SA string `json:"serviceAccountNamespace"`
}{

View file

@ -11,6 +11,11 @@ import (
//Query the JSON context with JMESPATH search path
func (ctx *Context) Query(query string) (interface{}, error) {
var emptyResult interface{}
// check for white-listed variables
if ctx.isWhiteListed(query) {
return emptyResult, fmt.Errorf("variable %s cannot be used", query)
}
// compile the query
queryPath, err := jmespath.Compile(query)
if err != nil {
@ -34,3 +39,12 @@ func (ctx *Context) Query(query string) (interface{}, error) {
}
return result, nil
}
func (ctx *Context) isWhiteListed(variable string) bool {
for _, wVar := range ctx.whiteListVars {
if wVar == variable {
return true
}
}
return false
}

View file

@ -1,15 +1,12 @@
package engine
import (
"fmt"
"time"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -33,15 +30,15 @@ func filterRule(rule kyverno.Rule, resource unstructured.Unstructured, admission
startTime := time.Now()
if !rbac.MatchAdmissionInfo(rule, admissionInfo) {
return nil
}
if !MatchesResourceDescription(resource, rule) {
if err := MatchesResourceDescription(resource, rule, admissionInfo); err != nil {
glog.V(4).Infof(err.Error())
return nil
}
// operate on the copy of the conditions, as we perform variable substitution
copyConditions := copyConditions(rule.Conditions)
// evaluate pre-conditions
if !variables.EvaluateConditions(ctx, rule.Conditions) {
if !variables.EvaluateConditions(ctx, copyConditions) {
glog.V(4).Infof("resource %s/%s does not satisfy the conditions for the rule ", resource.GetNamespace(), resource.GetName())
return nil
}
@ -69,13 +66,6 @@ func filterRules(policy kyverno.ClusterPolicy, resource unstructured.Unstructure
}
for _, rule := range policy.Spec.Rules {
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in generate rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if ruleResp := filterRule(rule, resource, admissionInfo, ctx); ruleResp != nil {
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp)
}

View file

@ -14,40 +14,22 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
jsonpatch "github.com/evanphx/json-patch"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/anchor"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
// ProcessOverlay processes mutation overlay on the resource
func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstructured.Unstructured) (resp response.RuleResponse, patchedResource unstructured.Unstructured) {
func ProcessOverlay(ruleName string, overlay interface{}, resource unstructured.Unstructured) (resp response.RuleResponse, patchedResource unstructured.Unstructured) {
startTime := time.Now()
glog.V(4).Infof("started applying overlay rule %q (%v)", rule.Name, startTime)
resp.Name = rule.Name
glog.V(4).Infof("started applying overlay rule %q (%v)", ruleName, startTime)
resp.Name = ruleName
resp.Type = utils.Mutation.String()
defer func() {
resp.RuleStats.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("finished applying overlay rule %q (%v)", resp.Name, resp.RuleStats.ProcessingTime)
}()
// if referenced path not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, rule.Mutation.Overlay); len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", invalidPaths)
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
return resp, resource
}
// substitute variables
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}
// if a JMESPATH fails, we dont return error but variable is substitured with nil and error log
overlay := variables.SubstituteVariables(ctx, rule.Mutation.Overlay)
patches, overlayerr := processOverlayPatches(resource.UnstructuredContent(), overlay)
// resource does not satisfy the overlay pattern, we don't apply this rule
if !reflect.DeepEqual(overlayerr, overlayError{}) {
@ -55,19 +37,19 @@ func ProcessOverlay(ctx context.EvalInterface, rule kyverno.Rule, resource unstr
// condition key is not present in the resource, don't apply this rule
// consider as success
case conditionNotPresent:
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg())
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", ruleName, resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg())
resp.Success = true
return resp, resource
// conditions are not met, don't apply this rule
case conditionFailure:
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg())
glog.V(3).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", ruleName, resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg())
//TODO: send zero response and not consider this as applied?
resp.Success = true
resp.Message = overlayerr.ErrorMsg()
return resp, resource
// rule application failed
case overlayFailure:
glog.Errorf("Resource %s/%s/%s: failed to process overlay: %v in the rule %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg(), rule.Name)
glog.Errorf("Resource %s/%s/%s: failed to process overlay: %v in the rule %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg(), ruleName)
resp.Success = false
resp.Message = fmt.Sprintf("failed to process overlay: %v", overlayerr.ErrorMsg())
return resp, resource

View file

@ -1,7 +1,6 @@
package engine
import (
"fmt"
"reflect"
"strings"
"time"
@ -9,9 +8,7 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/mutate"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/variables"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -36,72 +33,60 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
glog.V(4).Infof("started applying mutation rules of policy %q (%v)", policy.Name, startTime)
defer endMutateResultResponse(&resp, startTime)
incrementAppliedRuleCount := func() {
// rules applied successfully count
resp.PolicyResponse.RulesAppliedCount++
}
patchedResource := policyContext.NewResource
for _, rule := range policy.Spec.Rules {
var ruleResponse response.RuleResponse
//TODO: to be checked before calling the resources as well
if !rule.HasMutate() && !strings.Contains(PodControllers, resource.GetKind()) {
continue
}
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Mutation.String(), fmt.Sprintf("path not present in rule info: %s", paths)))
continue
}
startTime := time.Now()
if !rbac.MatchAdmissionInfo(rule, policyContext.AdmissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), policyContext.AdmissionInfo)
continue
}
glog.V(4).Infof("Time: Mutate matchAdmissionInfo %v", time.Since(startTime))
// check if the resource satisfies the filter conditions defined in the rule
//TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that
// dont statisfy a policy rule resource description
ok := MatchesResourceDescription(resource, rule)
if !ok {
glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resource.GetNamespace(), resource.GetName())
if err := MatchesResourceDescription(resource, rule, policyContext.AdmissionInfo); err != nil {
glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule:\n%s", resource.GetNamespace(), resource.GetName(), err.Error())
continue
}
// operate on the copy of the conditions, as we perform variable substitution
copyConditions := copyConditions(rule.Conditions)
// evaluate pre-conditions
if !variables.EvaluateConditions(ctx, rule.Conditions) {
// - handle variable subsitutions
if !variables.EvaluateConditions(ctx, copyConditions) {
glog.V(4).Infof("resource %s/%s does not satisfy the conditions for the rule ", resource.GetNamespace(), resource.GetName())
continue
}
mutation := rule.Mutation.DeepCopy()
// Process Overlay
if rule.Mutation.Overlay != nil {
var ruleResponse response.RuleResponse
ruleResponse, patchedResource = mutate.ProcessOverlay(ctx, rule, patchedResource)
if ruleResponse.Success {
// - variable substitution path is not present
if ruleResponse.PathNotPresent {
glog.V(4).Infof(ruleResponse.Message)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
continue
}
if mutation.Overlay != nil {
overlay := mutation.Overlay
// subsiitue the variables
var err error
if overlay, err = variables.SubstituteVars(ctx, overlay); err != nil {
// variable subsitution failed
ruleResponse.Success = false
ruleResponse.Message = err.Error()
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
continue
}
ruleResponse, patchedResource = mutate.ProcessOverlay(rule.Name, overlay, patchedResource)
if ruleResponse.Success {
// - overlay pattern does not match the resource conditions
if ruleResponse.Patches == nil {
glog.V(4).Infof(ruleResponse.Message)
continue
}
glog.Infof("Mutate overlay in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
glog.V(4).Infof("Mutate overlay in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
}
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
incrementAppliedRuleCount()
incrementAppliedRuleCount(&resp)
}
// Process Patches
@ -110,7 +95,7 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
ruleResponse, patchedResource = mutate.ProcessPatches(rule, patchedResource)
glog.Infof("Mutate patches in rule '%s' successfully applied on %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, ruleResponse)
incrementAppliedRuleCount()
incrementAppliedRuleCount(&resp)
}
// insert annotation to podtemplate if resource is pod controller
@ -121,7 +106,7 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
if strings.Contains(PodControllers, resource.GetKind()) {
var ruleResponse response.RuleResponse
ruleResponse, patchedResource = mutate.ProcessOverlay(ctx, podTemplateRule, patchedResource)
ruleResponse, patchedResource = mutate.ProcessOverlay(rule.Name, podTemplateRule, patchedResource)
if !ruleResponse.Success {
glog.Errorf("Failed to insert annotation to podTemplate of %s/%s/%s: %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), ruleResponse.Message)
continue
@ -137,6 +122,9 @@ func Mutate(policyContext PolicyContext) (resp response.EngineResponse) {
resp.PatchedResource = patchedResource
return resp
}
func incrementAppliedRuleCount(resp *response.EngineResponse) {
resp.PolicyResponse.RulesAppliedCount++
}
func startMutateResultResponse(resp *response.EngineResponse, policy kyverno.ClusterPolicy, resource unstructured.Unstructured) {
// set policy information

View file

@ -3,7 +3,6 @@ package engine
import (
"encoding/json"
"reflect"
"strings"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -152,52 +151,7 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) {
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
}
func Test_variableSubstitutionPathNotExist_InRuleInfo(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {
"name": "check-root-user"
}
}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"resources": {
"kinds": [
"{{request.kind}}"
]
}
}
}
]
}
}`)
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
ctx.AddResource(resourceRaw)
policyContext := PolicyContext{
Policy: policy,
Context: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
assert.Assert(t, strings.Contains(er.PolicyResponse.Rules[0].Message, "path not present in rule info"))
expectedErrorStr := "variable(s) not found or has nil values: [/spec/name/{{request.object.metadata.name1}}]"
t.Log(er.PolicyResponse.Rules[0].Message)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, expectedErrorStr)
}

View file

@ -1,68 +0,0 @@
package policy
import (
"fmt"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
//ContainsUserInfo returns error is userInfo is defined
func ContainsUserInfo(policy kyverno.ClusterPolicy) error {
// iterate of the policy rules to identify if userInfo is used
for idx, rule := range policy.Spec.Rules {
if err := userInfoDefined(rule.MatchResources.UserInfo); err != nil {
return fmt.Errorf("path: spec/rules[%d]/match/%s", idx, err)
}
if err := userInfoDefined(rule.ExcludeResources.UserInfo); err != nil {
return fmt.Errorf("path: spec/rules[%d]/exclude/%s", idx, err)
}
// variable defined with user information
// - condition.key
// - condition.value
// - mutate.overlay
// - validate.pattern
// - validate.anyPattern[*]
// variables to filter
// - request.userInfo*
// - serviceAccountName
// - serviceAccountNamespace
filterVars := []string{"request.userInfo*", "serviceAccountName", "serviceAccountNamespace"}
for condIdx, condition := range rule.Conditions {
if err := variables.CheckVariables(condition.Key, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/condition[%d]/key%s", idx, condIdx, err)
}
if err := variables.CheckVariables(condition.Value, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/condition[%d]/value%s", idx, condIdx, err)
}
}
if err := variables.CheckVariables(rule.Mutation.Overlay, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/mutate/overlay%s", idx, err)
}
if err := variables.CheckVariables(rule.Validation.Pattern, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/validate/pattern%s", idx, err)
}
for idx2, pattern := range rule.Validation.AnyPattern {
if err := variables.CheckVariables(pattern, filterVars, "/"); err != nil {
return fmt.Errorf("path: spec/rules[%d]/validate/anyPattern[%d]%s", idx, idx2, err)
}
}
}
return nil
}
func userInfoDefined(ui kyverno.UserInfo) error {
if len(ui.Roles) > 0 {
return fmt.Errorf("roles")
}
if len(ui.ClusterRoles) > 0 {
return fmt.Errorf("clusterRoles")
}
if len(ui.Subjects) > 0 {
return fmt.Errorf("subjects")
}
return nil
}

View file

@ -1,117 +0,0 @@
package rbac
import (
"reflect"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
utils "github.com/nirmata/kyverno/pkg/utils"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
)
const (
//SaPrefix defines the prefix for service accounts
SaPrefix = "system:serviceaccount:"
)
// MatchAdmissionInfo return true if the rule can be applied to the request
func MatchAdmissionInfo(rule kyverno.Rule, requestInfo kyverno.RequestInfo) bool {
// when processing existing resource, it does not contain requestInfo
// skip permission checking
if reflect.DeepEqual(requestInfo, kyverno.RequestInfo{}) {
return true
}
if !validateMatch(rule.MatchResources, requestInfo) {
return false
}
return validateExclude(rule.ExcludeResources, requestInfo)
}
// match:
// roles: role1, role2
// clusterRoles: clusterRole1,clusterRole2
// subjects: subject1, subject2
// validateMatch return true if (role1 || role2) and (clusterRole1 || clusterRole2)
// and (subject1 || subject2) are found in requestInfo, OR operation for each list
func validateMatch(match kyverno.MatchResources, requestInfo kyverno.RequestInfo) bool {
if len(match.Roles) > 0 {
if !matchRoleRefs(match.Roles, requestInfo.Roles) {
return false
}
}
if len(match.ClusterRoles) > 0 {
if !matchRoleRefs(match.ClusterRoles, requestInfo.ClusterRoles) {
return false
}
}
if len(match.Subjects) > 0 {
if !matchSubjects(match.Subjects, requestInfo.AdmissionUserInfo) {
return false
}
}
return true
}
// exclude:
// roles: role1, role2
// clusterRoles: clusterRole1,clusterRole2
// subjects: subject1, subject2
// validateExclude return true if none of the above found in requestInfo
// otherwise return false immediately means rule should not be applied
func validateExclude(exclude kyverno.ExcludeResources, requestInfo kyverno.RequestInfo) bool {
if len(exclude.Roles) > 0 {
if matchRoleRefs(exclude.Roles, requestInfo.Roles) {
return false
}
}
if len(exclude.ClusterRoles) > 0 {
if matchRoleRefs(exclude.ClusterRoles, requestInfo.ClusterRoles) {
return false
}
}
if len(exclude.Subjects) > 0 {
if matchSubjects(exclude.Subjects, requestInfo.AdmissionUserInfo) {
return false
}
}
return true
}
// matchRoleRefs return true if one of ruleRoleRefs exist in resourceRoleRefs
func matchRoleRefs(ruleRoleRefs, resourceRoleRefs []string) bool {
for _, ruleRoleRef := range ruleRoleRefs {
if utils.ContainsString(resourceRoleRefs, ruleRoleRef) {
return true
}
}
return false
}
// matchSubjects return true if one of ruleSubjects exist in userInfo
func matchSubjects(ruleSubjects []rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
userGroups := append(userInfo.Groups, userInfo.Username)
for _, subject := range ruleSubjects {
switch subject.Kind {
case "ServiceAccount":
if len(userInfo.Username) <= len(SaPrefix) {
continue
}
subjectServiceAccount := subject.Namespace + ":" + subject.Name
if userInfo.Username[len(SaPrefix):] == subjectServiceAccount {
return true
}
case "User", "Group":
if utils.ContainsString(userGroups, subject.Name) {
return true
}
}
}
return false
}

View file

@ -1,305 +0,0 @@
package rbac
import (
"flag"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
)
func Test_matchAdmissionInfo(t *testing.T) {
flag.Parse()
flag.Set("logtostderr", "true")
flag.Set("v", "3")
tests := []struct {
rule kyverno.Rule
info kyverno.RequestInfo
expected bool
}{
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{},
},
info: kyverno.RequestInfo{},
expected: true,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a"},
},
},
},
info: kyverno.RequestInfo{
Roles: []string{"ns-a:role-a"},
},
expected: true,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a"},
},
},
},
info: kyverno.RequestInfo{
Roles: []string{"ns-a:role"},
},
expected: false,
},
{
rule: kyverno.Rule{
MatchResources: kyverno.MatchResources{
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "serviceaccount:mynamespace:mysa",
},
},
expected: false,
},
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
UID: "1",
},
},
expected: true,
},
{
rule: kyverno.Rule{
ExcludeResources: kyverno.ExcludeResources{
UserInfo: kyverno.UserInfo{
Subjects: testSubjects(),
},
},
},
info: kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
},
},
expected: false,
},
}
for _, test := range tests {
assert.Assert(t, test.expected == MatchAdmissionInfo(test.rule, test.info))
}
}
func Test_validateMatch(t *testing.T) {
requestInfo := []struct {
info kyverno.RequestInfo
expected bool
}{
{
info: kyverno.RequestInfo{
Roles: []string{},
},
expected: false,
},
{
info: kyverno.RequestInfo{
Roles: []string{"ns-b:role-b"},
},
expected: true,
},
{
info: kyverno.RequestInfo{
Roles: []string{"ns:role"},
},
expected: false,
},
}
matchRoles := kyverno.MatchResources{
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateMatch(matchRoles, info.info))
}
requestInfo = []struct {
info kyverno.RequestInfo
expected bool
}{
{
info: kyverno.RequestInfo{
ClusterRoles: []string{},
},
expected: false,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"role-b"},
},
expected: false,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"clusterrole-b"},
},
expected: true,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
expected: true,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"fake-a", "fake-b"},
},
expected: false,
},
}
matchClusterRoles := kyverno.MatchResources{
UserInfo: kyverno.UserInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateMatch(matchClusterRoles, info.info))
}
}
func Test_validateExclude(t *testing.T) {
requestInfo := []struct {
info kyverno.RequestInfo
expected bool
}{
{
info: kyverno.RequestInfo{
Roles: []string{},
},
expected: true,
},
{
info: kyverno.RequestInfo{
Roles: []string{"ns-b:role-b"},
},
expected: false,
},
{
info: kyverno.RequestInfo{
Roles: []string{"ns:role"},
},
expected: true,
},
}
excludeRoles := kyverno.ExcludeResources{
UserInfo: kyverno.UserInfo{
Roles: []string{"ns-a:role-a", "ns-b:role-b"},
},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateExclude(excludeRoles, info.info))
}
requestInfo = []struct {
info kyverno.RequestInfo
expected bool
}{
{
info: kyverno.RequestInfo{
ClusterRoles: []string{},
},
expected: true,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"role-b"},
},
expected: true,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"clusterrole-b"},
},
expected: false,
},
{
info: kyverno.RequestInfo{
ClusterRoles: []string{"fake-a", "fake-b"},
},
expected: true,
},
}
excludeClusterRoles := kyverno.ExcludeResources{
UserInfo: kyverno.UserInfo{
ClusterRoles: []string{"clusterrole-a", "clusterrole-b"},
},
}
for _, info := range requestInfo {
assert.Assert(t, info.expected == validateExclude(excludeClusterRoles, info.info))
}
}
func Test_matchSubjects(t *testing.T) {
group := authenticationv1.UserInfo{
Username: "kubernetes-admin",
Groups: []string{"system:masters", "system:authenticated"},
}
sa := authenticationv1.UserInfo{
Username: "system:serviceaccount:mynamespace:mysa",
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:mynamespace", "system:authenticated"},
}
user := authenticationv1.UserInfo{
Username: "system:kube-scheduler",
Groups: []string{"system:authenticated"},
}
subjects := testSubjects()
assert.Assert(t, matchSubjects(subjects, sa))
assert.Assert(t, !matchSubjects(subjects, user))
assert.Assert(t, matchSubjects(subjects, group))
}
func testSubjects() []rbacv1.Subject {
return []rbacv1.Subject{
{
Kind: "User",
Name: "kube-scheduler",
},
{
Kind: "Group",
Name: "system:masters",
},
{
Kind: "ServiceAccount",
Name: "mysa",
Namespace: "mynamespace",
},
}
}

View file

@ -65,8 +65,6 @@ type RuleResponse struct {
Success bool `json:"success"`
// statistics
RuleStats `json:",inline"`
// PathNotPresent indicates whether referenced path in variable substitution exist
PathNotPresent bool `json:"pathNotPresent"`
}
//ToString ...
@ -121,13 +119,3 @@ func (er EngineResponse) getRules(success bool) []string {
}
return rules
}
// IsPathNotPresent checks if the referenced path(in variable substitution) exist
func (er EngineResponse) IsPathNotPresent() bool {
for _, r := range er.PolicyResponse.Rules {
if r.PathNotPresent {
return true
}
}
return false
}

View file

@ -1,18 +1,19 @@
package engine
import (
"encoding/json"
"strings"
"errors"
"fmt"
"reflect"
"time"
"github.com/nirmata/kyverno/pkg/utils"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
"github.com/golang/glog"
"github.com/minio/minio/pkg/wildcard"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -26,236 +27,176 @@ type EngineStats struct {
RulesAppliedCount int
}
//MatchesResourceDescription checks if the resource matches resource desription of the rule or not
func MatchesResourceDescription(resource unstructured.Unstructured, rule kyverno.Rule) bool {
matches := rule.MatchResources.ResourceDescription
exclude := rule.ExcludeResources.ResourceDescription
if len(matches.Kinds) > 0 {
if !findKind(matches.Kinds, resource.GetKind()) {
return false
}
}
name := resource.GetName()
namespace := resource.GetNamespace()
if matches.Name != "" {
// Matches
if !wildcard.Match(matches.Name, name) {
return false
}
}
// Matches
// check if the resource namespace is defined in the list of namespace pattern
if len(matches.Namespaces) > 0 && !utils.ContainsNamepace(matches.Namespaces, namespace) {
return false
}
// Matches
if matches.Selector != nil {
selector, err := metav1.LabelSelectorAsSelector(matches.Selector)
if err != nil {
glog.Error(err)
return false
}
if !selector.Matches(labels.Set(resource.GetLabels())) {
return false
}
}
excludeName := func(name string) Condition {
if exclude.Name == "" {
return NotEvaluate
}
if wildcard.Match(exclude.Name, name) {
return Skip
}
return Process
}
excludeNamespace := func(namespace string) Condition {
if len(exclude.Namespaces) == 0 {
return NotEvaluate
}
if utils.ContainsNamepace(exclude.Namespaces, namespace) {
return Skip
}
return Process
}
excludeSelector := func(labelsMap map[string]string) Condition {
if exclude.Selector == nil {
return NotEvaluate
}
selector, err := metav1.LabelSelectorAsSelector(exclude.Selector)
// if the label selector is incorrect, should be fail or
if err != nil {
glog.Error(err)
return Skip
}
if selector.Matches(labels.Set(labelsMap)) {
return Skip
}
return Process
}
excludeKind := func(kind string) Condition {
if len(exclude.Kinds) == 0 {
return NotEvaluate
}
if findKind(exclude.Kinds, kind) {
return Skip
}
return Process
}
// 0 -> dont check
// 1 -> is not to be exclude
// 2 -> to be exclude
excludeEval := []Condition{}
if ret := excludeName(resource.GetName()); ret != NotEvaluate {
excludeEval = append(excludeEval, ret)
}
if ret := excludeNamespace(resource.GetNamespace()); ret != NotEvaluate {
excludeEval = append(excludeEval, ret)
}
if ret := excludeSelector(resource.GetLabels()); ret != NotEvaluate {
excludeEval = append(excludeEval, ret)
}
if ret := excludeKind(resource.GetKind()); ret != NotEvaluate {
excludeEval = append(excludeEval, ret)
}
// Filtered NotEvaluate
if len(excludeEval) == 0 {
// nothing to exclude
return true
}
return func() bool {
for _, ret := range excludeEval {
if ret == Process {
return true
}
}
return false
}()
}
//Condition type for conditions
type Condition int
const (
// NotEvaluate to not-evaluate to condition
NotEvaluate Condition = 0
// Process to process the condition
Process Condition = 1
// Skip to skip the condition
Skip Condition = 2
)
// ParseResourceInfoFromObject get kind/namepace/name from resource
func ParseResourceInfoFromObject(rawResource []byte) string {
kind := ParseKindFromObject(rawResource)
namespace := ParseNamespaceFromObject(rawResource)
name := ParseNameFromObject(rawResource)
return strings.Join([]string{kind, namespace, name}, "/")
}
//ParseKindFromObject get kind from resource
func ParseKindFromObject(bytes []byte) string {
var objectJSON map[string]interface{}
json.Unmarshal(bytes, &objectJSON)
return objectJSON["kind"].(string)
}
//ParseNameFromObject extracts resource name from JSON obj
func ParseNameFromObject(bytes []byte) string {
var objectJSON map[string]interface{}
json.Unmarshal(bytes, &objectJSON)
meta, ok := objectJSON["metadata"]
if !ok {
return ""
}
metaMap, ok := meta.(map[string]interface{})
if !ok {
return ""
}
if name, ok := metaMap["name"].(string); ok {
return name
}
return ""
}
// ParseNamespaceFromObject extracts the namespace from the JSON obj
func ParseNamespaceFromObject(bytes []byte) string {
var objectJSON map[string]interface{}
json.Unmarshal(bytes, &objectJSON)
meta, ok := objectJSON["metadata"]
if !ok {
return ""
}
metaMap, ok := meta.(map[string]interface{})
if !ok {
return ""
}
if name, ok := metaMap["namespace"].(string); ok {
return name
}
return ""
}
func findKind(kinds []string, kindGVK string) bool {
func checkKind(kinds []string, resourceKind string) bool {
for _, kind := range kinds {
if kind == kindGVK {
if resourceKind == kind {
return true
}
}
return false
}
func checkName(name, resourceName string) bool {
return wildcard.Match(name, resourceName)
}
func checkNameSpace(namespaces []string, resourceNameSpace string) bool {
for _, namespace := range namespaces {
if resourceNameSpace == namespace {
return true
}
}
return false
}
// validateGeneralRuleInfoVariables validate variable subtition defined in
// - MatchResources
// - ExcludeResources
// - Conditions
func validateGeneralRuleInfoVariables(ctx context.EvalInterface, rule kyverno.Rule) string {
var tempRule kyverno.Rule
var tempRulePattern interface{}
tempRule.MatchResources = rule.MatchResources
tempRule.ExcludeResources = rule.ExcludeResources
tempRule.Conditions = rule.Conditions
raw, err := json.Marshal(tempRule)
func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) (bool, error) {
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
glog.Error(err)
return false, err
}
if err := json.Unmarshal(raw, &tempRulePattern); err != nil {
glog.Infof("failed to serilize rule info while validating variable substitution: %v", err)
return ""
if selector.Matches(labels.Set(resourceLabels)) {
return true, nil
}
return variables.ValidateVariables(ctx, tempRulePattern)
return false, nil
}
func newPathNotPresentRuleResponse(rname, rtype, msg string) response.RuleResponse {
return response.RuleResponse{
Name: rname,
Type: rtype,
Message: msg,
Success: true,
PathNotPresent: true,
func doesResourceMatchConditionBlock(conditionBlock kyverno.ResourceDescription, userInfo kyverno.UserInfo, admissionInfo kyverno.RequestInfo, resource unstructured.Unstructured) []error {
var errs []error
if len(conditionBlock.Kinds) > 0 {
if !checkKind(conditionBlock.Kinds, resource.GetKind()) {
errs = append(errs, fmt.Errorf("resource kind does not match conditionBlock"))
}
}
if conditionBlock.Name != "" {
if !checkName(conditionBlock.Name, resource.GetName()) {
errs = append(errs, fmt.Errorf("resource name does not match conditionBlock"))
}
}
if len(conditionBlock.Namespaces) > 0 {
if !checkNameSpace(conditionBlock.Namespaces, resource.GetNamespace()) {
errs = append(errs, fmt.Errorf("resource namespace does not match conditionBlock"))
}
}
if conditionBlock.Selector != nil {
hasPassed, err := checkSelector(conditionBlock.Selector, resource.GetLabels())
if err != nil {
errs = append(errs, fmt.Errorf("could not parse selector block of the policy in conditionBlock: %v", err))
} else {
if !hasPassed {
errs = append(errs, fmt.Errorf("resource does not match selector of given conditionBlock"))
}
}
}
if len(userInfo.Roles) > 0 {
if !doesSliceContainsAnyOfTheseValues(userInfo.Roles, admissionInfo.Roles...) {
errs = append(errs, fmt.Errorf("user info does not match roles for the given conditionBlock"))
}
}
if len(userInfo.ClusterRoles) > 0 {
if !doesSliceContainsAnyOfTheseValues(userInfo.ClusterRoles, admissionInfo.ClusterRoles...) {
errs = append(errs, fmt.Errorf("user info does not match clustersRoles for the given conditionBlock"))
}
}
if len(userInfo.Subjects) > 0 {
if !matchSubjects(userInfo.Subjects, admissionInfo.AdmissionUserInfo) {
errs = append(errs, fmt.Errorf("user info does not match subject for the given conditionBlock"))
}
}
return errs
}
// matchSubjects return true if one of ruleSubjects exist in userInfo
func matchSubjects(ruleSubjects []rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
const SaPrefix = "system:serviceaccount:"
userGroups := append(userInfo.Groups, userInfo.Username)
for _, subject := range ruleSubjects {
switch subject.Kind {
case "ServiceAccount":
if len(userInfo.Username) <= len(SaPrefix) {
continue
}
subjectServiceAccount := subject.Namespace + ":" + subject.Name
if userInfo.Username[len(SaPrefix):] == subjectServiceAccount {
return true
}
case "User", "Group":
if utils.ContainsString(userGroups, subject.Name) {
return true
}
}
}
return false
}
func doesSliceContainsAnyOfTheseValues(slice []string, values ...string) bool {
var sliceElementsMap = make(map[string]bool, len(slice))
for _, sliceElement := range slice {
sliceElementsMap[sliceElement] = true
}
for _, value := range values {
if sliceElementsMap[value] {
return true
}
}
return false
}
//MatchesResourceDescription checks if the resource matches resource description of the rule or not
func MatchesResourceDescription(resourceRef unstructured.Unstructured, ruleRef kyverno.Rule, admissionInfoRef kyverno.RequestInfo) error {
rule := *ruleRef.DeepCopy()
resource := *resourceRef.DeepCopy()
admissionInfo := *admissionInfoRef.DeepCopy()
var reasonsForFailure []error
if reflect.DeepEqual(admissionInfo, kyverno.RequestInfo{}) {
rule.MatchResources.UserInfo = kyverno.UserInfo{}
}
// checking if resource matches the rule
if !reflect.DeepEqual(rule.MatchResources.ResourceDescription, kyverno.ResourceDescription{}) {
matchErrs := doesResourceMatchConditionBlock(rule.MatchResources.ResourceDescription, rule.MatchResources.UserInfo, admissionInfo, resource)
reasonsForFailure = append(reasonsForFailure, matchErrs...)
} else {
reasonsForFailure = append(reasonsForFailure, fmt.Errorf("match block in rule cannot be empty"))
}
// checking if resource has been excluded
if !reflect.DeepEqual(rule.ExcludeResources.ResourceDescription, kyverno.ResourceDescription{}) {
excludeErrs := doesResourceMatchConditionBlock(rule.ExcludeResources.ResourceDescription, rule.ExcludeResources.UserInfo, admissionInfo, resource)
if excludeErrs == nil {
reasonsForFailure = append(reasonsForFailure, fmt.Errorf("resource has been excluded since it matches the exclude block"))
}
}
// creating final error
var errorMessage = "rule has failed to match resource for the following reasons:"
for i, reasonForFailure := range reasonsForFailure {
if reasonForFailure != nil {
errorMessage += "\n" + fmt.Sprint(i+1) + ". " + reasonForFailure.Error()
}
}
if len(reasonsForFailure) > 0 {
return errors.New(errorMessage)
}
return nil
}
func copyConditions(original []kyverno.Condition) []kyverno.Condition {
var copy []kyverno.Condition
for _, condition := range original {
copy = append(copy, *condition.DeepCopy())
}
return copy
}

View file

@ -2,17 +2,88 @@ package engine
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
context "github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/utils"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestMatchesResourceDescription(t *testing.T) {
tcs := []struct {
Description string
AdmissionInfo kyverno.RequestInfo
Resource []byte
Policy []byte
areErrorsExpected bool
}{
{
Description: "Should match pod and not exclude it",
AdmissionInfo: kyverno.RequestInfo{
ClusterRoles: []string{"admin"},
},
Resource: []byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"name":"hello-world","labels":{"name":"hello-world"}},"spec":{"containers":[{"name":"hello-world","image":"hello-world","ports":[{"containerPort":81}],"resources":{"limits":{"memory":"30Mi","cpu":"0.2"},"requests":{"memory":"20Mi","cpu":"0.1"}}}]}}`),
Policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"hello-world-policy"},"spec":{"background":false,"rules":[{"name":"hello-world-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"hello-world"},"clusterRoles":["system:node"]},"mutate":{"overlay":{"spec":{"containers":[{"(image)":"*","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
areErrorsExpected: false,
},
{
Description: "Should exclude resource since it matches the exclude block",
AdmissionInfo: kyverno.RequestInfo{
ClusterRoles: []string{"system:node"},
},
Resource: []byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"name":"hello-world","labels":{"name":"hello-world"}},"spec":{"containers":[{"name":"hello-world","image":"hello-world","ports":[{"containerPort":81}],"resources":{"limits":{"memory":"30Mi","cpu":"0.2"},"requests":{"memory":"20Mi","cpu":"0.1"}}}]}}`),
Policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"hello-world-policy"},"spec":{"background":false,"rules":[{"name":"hello-world-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"hello-world"},"clusterRoles":["system:node"]},"mutate":{"overlay":{"spec":{"containers":[{"(image)":"*","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
areErrorsExpected: true,
},
{
Description: "Should not fail if in sync mode, if admission info is empty it should still match resources with specific clusterRoles",
Resource: []byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"name":"hello-world","labels":{"name":"hello-world"}},"spec":{"containers":[{"name":"hello-world","image":"hello-world","ports":[{"containerPort":81}],"resources":{"limits":{"memory":"30Mi","cpu":"0.2"},"requests":{"memory":"20Mi","cpu":"0.1"}}}]}}`),
Policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"hello-world-policy"},"spec":{"background":false,"rules":[{"name":"hello-world-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"hello-world"},"clusterRoles":["system:node"]},"mutate":{"overlay":{"spec":{"containers":[{"(image)":"*","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
areErrorsExpected: false,
},
{
Description: "Should fail since resource does not match policy",
AdmissionInfo: kyverno.RequestInfo{
ClusterRoles: []string{"admin"},
},
Resource: []byte(`{"apiVersion":"v1","kind":"Service","metadata":{"name":"hello-world","labels":{"name":"hello-world"}},"spec":{"containers":[{"name":"hello-world","image":"hello-world","ports":[{"containerPort":81}],"resources":{"limits":{"memory":"30Mi","cpu":"0.2"},"requests":{"memory":"20Mi","cpu":"0.1"}}}]}}`),
Policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"hello-world-policy"},"spec":{"background":false,"rules":[{"name":"hello-world-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"hello-world"},"clusterRoles":["system:node"]},"mutate":{"overlay":{"spec":{"containers":[{"(image)":"*","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
areErrorsExpected: true,
},
{
Description: "Should not fail since resource does not match exclude block",
AdmissionInfo: kyverno.RequestInfo{
ClusterRoles: []string{"system:node"},
},
Resource: []byte(`{"apiVersion":"v1","kind":"Pod","metadata":{"name":"hello-world2","labels":{"name":"hello-world"}},"spec":{"containers":[{"name":"hello-world","image":"hello-world","ports":[{"containerPort":81}],"resources":{"limits":{"memory":"30Mi","cpu":"0.2"},"requests":{"memory":"20Mi","cpu":"0.1"}}}]}}`),
Policy: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"hello-world-policy"},"spec":{"background":false,"rules":[{"name":"hello-world-policy","match":{"resources":{"kinds":["Pod"]}},"exclude":{"resources":{"name":"hello-world"},"clusterRoles":["system:node"]},"mutate":{"overlay":{"spec":{"containers":[{"(image)":"*","imagePullPolicy":"IfNotPresent"}]}}}}]}}`),
areErrorsExpected: false,
},
}
for i, tc := range tcs {
var policy kyverno.Policy
err := json.Unmarshal(tc.Policy, &policy)
if err != nil {
t.Errorf("Testcase %d invalid policy raw", i+1)
}
resource, _ := utils.ConvertToUnstructured(tc.Resource)
for _, rule := range policy.Spec.Rules {
err := MatchesResourceDescription(*resource, rule, tc.AdmissionInfo)
if err != nil {
if !tc.areErrorsExpected {
t.Errorf("Testcase %d Unexpected error: %v", i+1, err)
}
} else {
if tc.areErrorsExpected {
t.Errorf("Testcase %d Expected Error but recieved no error", i+1)
}
}
}
}
}
// Match multiple kinds
func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
rawResource := []byte(`{
@ -67,7 +138,10 @@ func TestResourceDescriptionMatch_MultipleKind(t *testing.T) {
}
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription}}
assert.Assert(t, MatchesResourceDescription(*resource, rule))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
// Match resource name
@ -125,7 +199,9 @@ func TestResourceDescriptionMatch_Name(t *testing.T) {
}
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription}}
assert.Assert(t, MatchesResourceDescription(*resource, rule))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
// Match resource regex
@ -183,7 +259,9 @@ func TestResourceDescriptionMatch_Name_Regex(t *testing.T) {
}
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription}}
assert.Assert(t, MatchesResourceDescription(*resource, rule))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
// Match expressions for labels to not match
@ -249,7 +327,9 @@ func TestResourceDescriptionMatch_Label_Expression_NotMatch(t *testing.T) {
}
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription}}
assert.Assert(t, MatchesResourceDescription(*resource, rule))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
// Match label expression in matching set
@ -316,7 +396,9 @@ func TestResourceDescriptionMatch_Label_Expression_Match(t *testing.T) {
}
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription}}
assert.Assert(t, MatchesResourceDescription(*resource, rule))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err != nil {
t.Errorf("Testcase has failed due to the following:%v", err)
}
}
// check for exclude conditions
@ -394,118 +476,7 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) {
rule := kyverno.Rule{MatchResources: kyverno.MatchResources{ResourceDescription: resourceDescription},
ExcludeResources: kyverno.ExcludeResources{ResourceDescription: resourceDescriptionExclude}}
assert.Assert(t, !MatchesResourceDescription(*resource, rule))
}
func Test_validateGeneralRuleInfoVariables(t *testing.T) {
rawResource := []byte(`
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "image-with-hostpath",
"labels": {
"app.type": "prod",
"namespace": "my-namespace"
}
},
"spec": {
"containers": [
{
"name": "image-with-hostpath",
"image": "docker.io/nautiker/curl",
"volumeMounts": [
{
"name": "var-lib-etcd",
"mountPath": "/var/lib"
}
]
}
],
"volumes": [
{
"name": "var-lib-etcd",
"emptyDir": {}
}
]
}
}
`)
policyRaw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-validate-variables"
},
"spec": {
"rules": [
{
"name": "test-match",
"match": {
"Subjects": [
{
"kind": "User",
"name": "{{request.userInfo.username1}}}"
}
],
"resources": {
"kind": "{{request.object.kind}}"
}
}
},
{
"name": "test-exclude",
"match": {
"resources": {
"namespaces": [
"{{request.object.namespace}}"
]
}
}
},
{
"name": "test-condition",
"preconditions": [
{
"key": "{{serviceAccountName}}",
"operator": "NotEqual",
"value": "testuser"
}
]
}
]
}
}`)
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyRaw, &policy))
ctx := context.NewContext()
var err error
err = ctx.AddResource(rawResource)
if err != nil {
t.Error(err)
}
err = ctx.AddUserInfo(userReqInfo)
if err != nil {
t.Error(err)
}
err = ctx.AddSA("system:serviceaccount:test:testuser")
if err != nil {
t.Error(err)
}
expectPaths := []string{"request.userInfo.username1", "request.object.namespace", ""}
for i, rule := range policy.Spec.Rules {
invalidPaths := validateGeneralRuleInfoVariables(ctx, rule)
assert.Assert(t, invalidPaths == expectPaths[i], fmt.Sprintf("result not match, got invalidPaths %s", invalidPaths))
if err := MatchesResourceDescription(*resource, rule, kyverno.RequestInfo{}); err == nil {
t.Errorf("Testcase has failed due to the following:\n Function has returned no error, even though it was suposed to fail")
}
}

View file

@ -5,16 +5,6 @@ import (
"strconv"
)
//ValidationFailureReason defeins type for Validation Failure reason
type ValidationFailureReason int
const (
// PathNotPresent if path is not present
PathNotPresent ValidationFailureReason = iota
// Rulefailure if the rule failed
Rulefailure
)
// convertToString converts value to string
func convertToString(value interface{}) (string, error) {
switch typed := value.(type) {
@ -44,14 +34,3 @@ func getRawKeyIfWrappedWithAttributes(str string) string {
return str
}
}
//ValidationError stores error for validation error
type ValidationError struct {
StatusCode ValidationFailureReason
ErrorMsg string
}
// newValidatePatternError returns an validation error using the ValidationFailureReason and errorMsg
func newValidatePatternError(reason ValidationFailureReason, msg string) ValidationError {
return ValidationError{StatusCode: reason, ErrorMsg: msg}
}

View file

@ -10,29 +10,18 @@ import (
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/anchor"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/operator"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
// ValidateResourceWithPattern is a start of element-by-element validation process
// It assumes that validation is started from root, so "/" is passed
func ValidateResourceWithPattern(ctx context.EvalInterface, resource, pattern interface{}) (string, ValidationError) {
// if referenced path is not present, we skip processing the rule and report violation
if invalidPaths := variables.ValidateVariables(ctx, pattern); len(invalidPaths) != 0 {
return "", newValidatePatternError(PathNotPresent, invalidPaths)
}
// first pass we substitute all the JMESPATH substitution for the variable
// variable: {{<JMESPATH>}}
// if a JMESPATH fails, we dont return error but variable is substitured with nil and error log
pattern = variables.SubstituteVariables(ctx, pattern)
func ValidateResourceWithPattern(resource, pattern interface{}) (string, error) {
path, err := validateResourceElement(resource, pattern, pattern, "/")
if err != nil {
return path, newValidatePatternError(Rulefailure, err.Error())
return path, err
}
return "", ValidationError{}
return "", nil
}
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)
@ -71,7 +60,7 @@ func validateResourceElement(resourceElement, patternElement, originPattern inte
}
}
if !ValidateValueWithPattern(resourceElement, patternElement) {
return path, fmt.Errorf("Validation rule failed at '%s' to validate value %v with pattern %v", path, resourceElement, patternElement)
return path, fmt.Errorf("Validation rule failed at '%s' to validate value '%v' with pattern '%v'", path, resourceElement, patternElement)
}
default:

View file

@ -1,16 +1,13 @@
package engine
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/rbac"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/engine/utils"
"github.com/nirmata/kyverno/pkg/engine/validate"
@ -94,32 +91,21 @@ func validateResource(ctx context.EvalInterface, policy kyverno.ClusterPolicy, r
continue
}
startTime := time.Now()
if paths := validateGeneralRuleInfoVariables(ctx, rule); len(paths) != 0 {
glog.Infof("referenced path not present in rule %s/, resource %s/%s/%s, path: %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), paths)
resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules,
newPathNotPresentRuleResponse(rule.Name, utils.Validation.String(), fmt.Sprintf("path not present: %s", paths)))
continue
}
if !rbac.MatchAdmissionInfo(rule, admissionInfo) {
glog.V(3).Infof("rule '%s' cannot be applied on %s/%s/%s, admission permission: %v",
rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), admissionInfo)
continue
}
glog.V(4).Infof("Time: Validate matchAdmissionInfo %v", time.Since(startTime))
// check if the resource satisfies the filter conditions defined in the rule
// TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that
// dont statisfy a policy rule resource description
ok := MatchesResourceDescription(resource, rule)
if !ok {
glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule ", resource.GetNamespace(), resource.GetName())
if err := MatchesResourceDescription(resource, rule, admissionInfo); err != nil {
glog.V(4).Infof("resource %s/%s does not satisfy the resource description for the rule:\n%s", resource.GetNamespace(), resource.GetName(), err.Error())
continue
}
// operate on the copy of the conditions, as we perform variable substitution
copyConditions := copyConditions(rule.Conditions)
// evaluate pre-conditions
if !variables.EvaluateConditions(ctx, rule.Conditions) {
// - handle variable subsitutions
if !variables.EvaluateConditions(ctx, copyConditions) {
glog.V(4).Infof("resource %s/%s does not satisfy the conditions for the rule ", resource.GetNamespace(), resource.GetName())
continue
}
@ -182,27 +168,29 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
resp.RuleStats.ProcessingTime = time.Since(startTime)
glog.V(4).Infof("finished applying validation rule %q (%v)", resp.Name, resp.RuleStats.ProcessingTime)
}()
// work on a copy of validation rule
validationRule := rule.Validation.DeepCopy()
// either pattern or anyPattern can be specified in Validation rule
if rule.Validation.Pattern != nil {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, rule.Validation.Pattern)
if !reflect.DeepEqual(err, validate.ValidationError{}) {
switch err.StatusCode {
case validate.PathNotPresent:
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", err.ErrorMsg)
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
case validate.Rulefailure:
// rule application failed
glog.V(4).Infof("Validation rule '%s' failed at '%s' for resource %s/%s/%s. %s: %v", rule.Name, path, resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Validation.Message, err)
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
}
if validationRule.Pattern != nil {
// substitute variables in the pattern
pattern := validationRule.Pattern
var err error
if pattern, err = variables.SubstituteVars(ctx, pattern); err != nil {
// variable subsitution failed
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed. '%s'",
rule.Validation.Message, rule.Name, err)
return resp
}
if path, err := validate.ValidateResourceWithPattern(resource.Object, pattern); err != nil {
// validation failed
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed at path '%s'",
rule.Validation.Message, rule.Name, path)
return resp
}
// rule application successful
glog.V(4).Infof("rule %s pattern validated successfully on resource %s/%s/%s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName())
resp.Success = true
@ -210,55 +198,45 @@ func validatePatterns(ctx context.EvalInterface, resource unstructured.Unstructu
return resp
}
// using anyPattern we can define multiple patterns and only one of them has to be successfully validated
// return directly if one pattern succeed
// if none succeed, report violation / policyerror(TODO)
if rule.Validation.AnyPattern != nil {
var ruleFailureErrs []error
var failedPaths, invalidPaths []string
for index, pattern := range rule.Validation.AnyPattern {
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, pattern)
// this pattern was successfully validated
if reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("anyPattern %v successfully validated on resource %s/%s/%s", pattern, resource.GetKind(), resource.GetNamespace(), resource.GetName())
if validationRule.AnyPattern != nil {
var failedSubstitutionsErrors []error
var failedAnyPatternsErrors []error
var err error
for idx, pattern := range validationRule.AnyPattern {
if pattern, err = variables.SubstituteVars(ctx, pattern); err != nil {
// variable subsitution failed
failedSubstitutionsErrors = append(failedSubstitutionsErrors, err)
continue
}
_, err := validate.ValidateResourceWithPattern(resource.Object, pattern)
if err == nil {
resp.Success = true
resp.Message = fmt.Sprintf("Validation rule '%s' anyPattern[%d] succeeded.", rule.Name, index)
resp.Message = fmt.Sprintf("Validation rule '%s' anyPattern[%d] succeeded.", rule.Name, idx)
return resp
}
switch err.StatusCode {
case validate.PathNotPresent:
invalidPaths = append(invalidPaths, err.ErrorMsg)
case validate.Rulefailure:
glog.V(4).Infof("Validation error: %s; Validation rule %s anyPattern[%d] failed at path %s for %s/%s/%s",
rule.Validation.Message, rule.Name, index, path, resource.GetKind(), resource.GetNamespace(), resource.GetName())
ruleFailureErrs = append(ruleFailureErrs, errors.New(err.ErrorMsg))
failedPaths = append(failedPaths, path)
}
glog.V(4).Infof("Validation error: %s; Validation rule %s anyPattern[%d] for %s/%s/%s",
rule.Validation.Message, rule.Name, idx, resource.GetKind(), resource.GetNamespace(), resource.GetName())
patternErr := fmt.Errorf("anyPattern[%d] failed; %s", idx, err)
failedAnyPatternsErrors = append(failedAnyPatternsErrors, patternErr)
}
// PathNotPresent
if len(invalidPaths) != 0 {
resp.Success = true
resp.PathNotPresent = true
resp.Message = fmt.Sprintf("referenced path not present: %s", strings.Join(invalidPaths, ";"))
glog.V(4).Infof("Skip applying rule '%s' on resource '%s/%s/%s': %s", rule.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), resp.Message)
// Subsitution falures
if len(failedSubstitutionsErrors) > 0 {
resp.Success = false
resp.Message = fmt.Sprintf("Subsitutions failed at paths: %v", failedSubstitutionsErrors)
return resp
}
// none of the anyPatterns succeed: len(ruleFailureErrs) > 0
glog.V(4).Infof("none of anyPattern comply with resource: %v", ruleFailureErrs)
resp.Success = false
var errorStr []string
for index, err := range ruleFailureErrs {
glog.V(4).Infof("anyPattern[%d] failed at path %s: %v", index, failedPaths[index], err)
str := fmt.Sprintf("Validation rule %s anyPattern[%d] failed at path %s.", rule.Name, index, failedPaths[index])
errorStr = append(errorStr, str)
// Any Pattern validation errors
if len(failedAnyPatternsErrors) > 0 {
var errorStr []string
for _, err := range failedAnyPatternsErrors {
errorStr = append(errorStr, err.Error())
}
resp.Success = false
resp.Message = fmt.Sprintf("Validation rule '%s' failed. %s", rule.Name, errorStr)
return resp
}
resp.Message = fmt.Sprintf("Validation error: %s; %s", rule.Validation.Message, strings.Join(errorStr, " "))
return resp
}
return response.RuleResponse{}
}

View file

@ -294,7 +294,7 @@ func TestValidate_Fail_anyPattern(t *testing.T) {
resourceUnstructured, err := utils.ConvertToUnstructured(rawResource)
assert.NilError(t, err)
er := Validate(PolicyContext{Policy: policy, NewResource: *resourceUnstructured})
msgs := []string{"Validation error: A namespace is required; Validation rule check-default-namespace anyPattern[0] failed at path /metadata/namespace/. Validation rule check-default-namespace anyPattern[1] failed at path /metadata/namespace/."}
msgs := []string{"Validation rule 'check-default-namespace' failed. [anyPattern[0] failed; Validation rule failed at '/metadata/namespace/' to validate value '<nil>' with pattern '?*' anyPattern[1] failed; Validation rule failed at '/metadata/namespace/' to validate value '<nil>' with pattern '!default']"}
for index, r := range er.PolicyResponse.Rules {
assert.Equal(t, r.Message, msgs[index])
}
@ -1309,8 +1309,8 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "Validation error: ; Validation rule 'test-path-not-exist' failed. 'variable(s) not found or has nil values: [/spec/containers/0/name/{{request.object.metadata.name1}}]'")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *testing.T) {
@ -1399,10 +1399,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfies(t *t
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success == true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
expectMsg := "Validation rule 'test-path-not-exist' anyPattern[1] succeeded."
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectMsg)
assert.Assert(t, er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "Validation rule 'test-path-not-exist' anyPattern[1] succeeded.")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *testing.T) {
@ -1491,8 +1489,8 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test
Context: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Assert(t, er.PolicyResponse.Rules[0].Success, true)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent, true)
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "Subsitutions failed at paths: [variable(s) not found or has nil values: [/spec/template/spec/containers/0/name/{{request.object.metadata.name1}}] variable(s) not found or has nil values: [/spec/template/spec/containers/0/name/{{request.object.metadata.name2}}]]")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {
@ -1582,8 +1580,7 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatter
NewResource: *resourceUnstructured}
er := Validate(policyContext)
expectedMsg := "Validation error: ; Validation rule test-path-not-exist anyPattern[0] failed at path /spec/template/spec/containers/0/name/. Validation rule test-path-not-exist anyPattern[1] failed at path /spec/template/spec/containers/0/name/."
assert.Assert(t, er.PolicyResponse.Rules[0].Success == false)
assert.Assert(t, er.PolicyResponse.Rules[0].PathNotPresent == false)
assert.Assert(t, er.PolicyResponse.Rules[0].Message == expectedMsg)
// expectedMsg := "Validation error: ; Validation rule test-path-not-exist anyPattern[0] failed at path /spec/template/spec/containers/0/name/. Validation rule test-path-not-exist anyPattern[1] failed at path /spec/template/spec/containers/0/name/."
assert.Assert(t, !er.PolicyResponse.Rules[0].Success)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "Validation rule 'test-path-not-exist' failed. [anyPattern[0] failed; Validation rule failed at '/spec/template/spec/containers/0/name/' to validate value 'pod-test-pod' with pattern 'test*' anyPattern[1] failed; Validation rule failed at '/spec/template/spec/containers/0/name/' to validate value 'pod-test-pod' with pattern 'test*']")
}

View file

@ -10,7 +10,7 @@ import (
//Evaluate evaluates the condition
func Evaluate(ctx context.EvalInterface, condition kyverno.Condition) bool {
// get handler for the operator
handle := operator.CreateOperatorHandler(ctx, condition.Operator, SubstituteVariables)
handle := operator.CreateOperatorHandler(ctx, condition.Operator, SubstituteVars)
if handle == nil {
return false
}

View file

@ -25,25 +25,36 @@ type EqualHandler struct {
//Evaluate evaluates expression with Equal Operator
func (eh EqualHandler) Evaluate(key, value interface{}) bool {
var err error
//TODO: decouple variables from evaluation
// substitute the variables
nKey := eh.subHandler(eh.ctx, key)
nValue := eh.subHandler(eh.ctx, value)
if key, err = eh.subHandler(eh.ctx, key); err != nil {
// Failed to resolve the variable
glog.Infof("Failed to resolve variables in key: %s: %v", key, err)
return false
}
if value, err = eh.subHandler(eh.ctx, value); err != nil {
// Failed to resolve the variable
glog.Infof("Failed to resolve variables in value: %s: %v", value, err)
return false
}
// key and value need to be of same type
switch typedKey := nKey.(type) {
switch typedKey := key.(type) {
case bool:
return eh.validateValuewithBoolPattern(typedKey, nValue)
return eh.validateValuewithBoolPattern(typedKey, value)
case int:
return eh.validateValuewithIntPattern(int64(typedKey), nValue)
return eh.validateValuewithIntPattern(int64(typedKey), value)
case int64:
return eh.validateValuewithIntPattern(typedKey, nValue)
return eh.validateValuewithIntPattern(typedKey, value)
case float64:
return eh.validateValuewithFloatPattern(typedKey, nValue)
return eh.validateValuewithFloatPattern(typedKey, value)
case string:
return eh.validateValuewithStringPattern(typedKey, nValue)
return eh.validateValuewithStringPattern(typedKey, value)
case map[string]interface{}:
return eh.validateValueWithMapPattern(typedKey, nValue)
return eh.validateValueWithMapPattern(typedKey, value)
case []interface{}:
return eh.validateValueWithSlicePattern(typedKey, nValue)
return eh.validateValueWithSlicePattern(typedKey, value)
default:
glog.Errorf("Unsupported type %v", typedKey)
return false

View file

@ -25,25 +25,35 @@ type NotEqualHandler struct {
//Evaluate evaluates expression with NotEqual Operator
func (neh NotEqualHandler) Evaluate(key, value interface{}) bool {
var err error
//TODO: decouple variables from evaluation
// substitute the variables
nKey := neh.subHandler(neh.ctx, key)
nValue := neh.subHandler(neh.ctx, value)
if key, err = neh.subHandler(neh.ctx, key); err != nil {
// Failed to resolve the variable
glog.Infof("Failed to resolve variables in key: %s: %v", key, err)
return false
}
if value, err = neh.subHandler(neh.ctx, value); err != nil {
// Failed to resolve the variable
glog.Infof("Failed to resolve variables in value: %s: %v", value, err)
return false
}
// key and value need to be of same type
switch typedKey := nKey.(type) {
switch typedKey := key.(type) {
case bool:
return neh.validateValuewithBoolPattern(typedKey, nValue)
return neh.validateValuewithBoolPattern(typedKey, value)
case int:
return neh.validateValuewithIntPattern(int64(typedKey), nValue)
return neh.validateValuewithIntPattern(int64(typedKey), value)
case int64:
return neh.validateValuewithIntPattern(typedKey, nValue)
return neh.validateValuewithIntPattern(typedKey, value)
case float64:
return neh.validateValuewithFloatPattern(typedKey, nValue)
return neh.validateValuewithFloatPattern(typedKey, value)
case string:
return neh.validateValuewithStringPattern(typedKey, nValue)
return neh.validateValuewithStringPattern(typedKey, value)
case map[string]interface{}:
return neh.validateValueWithMapPattern(typedKey, nValue)
return neh.validateValueWithMapPattern(typedKey, value)
case []interface{}:
return neh.validateValueWithSlicePattern(typedKey, nValue)
return neh.validateValueWithSlicePattern(typedKey, value)
default:
glog.Error("Unsupported type %V", typedKey)
return false

View file

@ -17,7 +17,7 @@ type OperatorHandler interface {
}
//VariableSubstitutionHandler defines the handler function for variable substitution
type VariableSubstitutionHandler = func(ctx context.EvalInterface, pattern interface{}) interface{}
type VariableSubstitutionHandler = func(ctx context.EvalInterface, pattern interface{}) (interface{}, error)
//CreateOperatorHandler returns the operator handler based on the operator used in condition
func CreateOperatorHandler(ctx context.EvalInterface, op kyverno.ConditionOperator, subHandler VariableSubstitutionHandler) OperatorHandler {

View file

@ -1,91 +0,0 @@
package variables
import (
"fmt"
"regexp"
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/context"
)
// ValidateVariables validates if referenced path is present
// return empty string if all paths are valid, otherwise return invalid path
func ValidateVariables(ctx context.EvalInterface, pattern interface{}) string {
var pathsNotPresent []string
variableList := extractVariables(pattern)
for _, variable := range variableList {
if len(variable) == 2 {
varName := variable[0]
varValue := variable[1]
glog.V(3).Infof("validating variable %s", varName)
val, err := ctx.Query(varValue)
if err == nil && val == nil {
// path is not present, returns nil interface
pathsNotPresent = append(pathsNotPresent, varValue)
}
}
}
if len(pathsNotPresent) != 0 {
return strings.Join(pathsNotPresent, ";")
}
return ""
}
// extractVariables extracts variables in the pattern
func extractVariables(pattern interface{}) [][]string {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return extractMap(typedPattern)
case []interface{}:
return extractArray(typedPattern)
case string:
return extractValue(typedPattern)
default:
fmt.Printf("variable type %T", typedPattern)
return nil
}
}
func extractMap(patternMap map[string]interface{}) [][]string {
var variableList [][]string
for _, patternElement := range patternMap {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractArray(patternList []interface{}) [][]string {
var variableList [][]string
for _, patternElement := range patternList {
if vars := extractVariables(patternElement); vars != nil {
variableList = append(variableList, vars...)
}
}
return variableList
}
func extractValue(valuePattern string) [][]string {
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
return extractValueVariable(variable)
}
// returns multiple variable match groups
func extractValueVariable(valuePattern string) [][]string {
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindAllStringSubmatch(valuePattern, -1)
if len(groups) == 0 {
// no variables
return nil
}
// group[*] <- all the matches
// group[*][0] <- group match
// group[*][1] <- group capture value
return groups
}

View file

@ -1,175 +0,0 @@
package variables
import (
"encoding/json"
"fmt"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"gotest.tools/assert"
authenticationv1 "k8s.io/api/authentication/v1"
)
func Test_ExtractVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name}}"
]
}
]
}
}
`)
var pattern interface{}
if err := json.Unmarshal(patternRaw, &pattern); err != nil {
t.Error(err)
}
vars := extractVariables(pattern)
result := [][]string{{"{{request.userInfo.username}}", "request.userInfo.username"},
{"{{request.object.metadata.name}}", "request.object.metadata.name"}}
assert.Assert(t, len(vars) == len(result), fmt.Sprintf("result does not match, var: %s", vars))
}
func Test_ValidateVariables_NoVariable(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"Pod"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
var err error
ctx := context.NewContext()
err = ctx.AddResource(resourceRaw)
if err != nil {
t.Error(err)
}
err = ctx.AddUserInfo(userReqInfo)
if err != nil {
t.Error(err)
}
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) == 0)
}
func Test_ValidateVariables(t *testing.T) {
patternRaw := []byte(`
{
"name": "ns-owner-{{request.userInfo.username}}",
"data": {
"rules": [
{
"apiGroups": [
""
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name1}}"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
// userInfo
userReqInfo := kyverno.RequestInfo{
AdmissionUserInfo: authenticationv1.UserInfo{
Username: "user1",
},
}
var pattern, resource interface{}
assert.NilError(t, json.Unmarshal(patternRaw, &pattern))
assert.NilError(t, json.Unmarshal(resourceRaw, &resource))
ctx := context.NewContext()
var err error
err = ctx.AddResource(resourceRaw)
if err != nil {
t.Error(err)
}
err = ctx.AddUserInfo(userReqInfo)
if err != nil {
t.Error(err)
}
invalidPaths := ValidateVariables(ctx, pattern)
assert.Assert(t, len(invalidPaths) > 0)
}

View file

@ -1,146 +0,0 @@
package variables
import (
"regexp"
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/operator"
)
const variableRegex = `\{\{([^{}]*)\}\}`
//SubstituteVariables substitutes the JMESPATH with variable substitution
// supported substitutions
// - no operator + variable(string,object)
// unsupported substitutions
// - operator + variable(object) -> as we dont support operators with object types
func SubstituteVariables(ctx context.EvalInterface, pattern interface{}) interface{} {
// var err error
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return substituteMap(ctx, typedPattern)
case []interface{}:
return substituteArray(ctx, typedPattern)
case string:
// variable substitution
return substituteValue(ctx, typedPattern)
default:
return pattern
}
}
func substituteMap(ctx context.EvalInterface, patternMap map[string]interface{}) map[string]interface{} {
for key, patternElement := range patternMap {
value := SubstituteVariables(ctx, patternElement)
patternMap[key] = value
}
return patternMap
}
func substituteArray(ctx context.EvalInterface, patternList []interface{}) []interface{} {
for idx, patternElement := range patternList {
value := SubstituteVariables(ctx, patternElement)
patternList[idx] = value
}
return patternList
}
func substituteValue(ctx context.EvalInterface, valuePattern string) interface{} {
// patterns supported
// - operator + string
// operator + variable
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
// substitute variable with value
value := getValueQuery(ctx, variable)
if operatorVariable == "" {
// default or operator.Equal
// equal + string variable
// object variable
return value
}
// operator + string variable
switch value.(type) {
case string:
return string(operatorVariable) + value.(string)
default:
glog.Infof("cannot use operator with object variables. operator used %s in pattern %v", string(operatorVariable), valuePattern)
var emptyInterface interface{}
return emptyInterface
}
}
func getValueQuery(ctx context.EvalInterface, valuePattern string) interface{} {
var emptyInterface interface{}
// extract variable {{<variable>}}
validRegex := regexp.MustCompile(variableRegex)
groups := validRegex.FindAllStringSubmatch(valuePattern, -1)
// can have multiple variables in a single value pattern
// var Map <variable,value>
varMap := getValues(ctx, groups)
if len(varMap) == 0 {
// there are no varaiables
// return the original value
return valuePattern
}
// only substitute values if all the variable values are of type string
if isAllVarStrings(varMap) {
newVal := valuePattern
for key, value := range varMap {
if val, ok := value.(string); ok {
newVal = strings.Replace(newVal, key, val, -1)
}
}
return newVal
}
// we do not support multiple substitution per statement for non-string types
for _, value := range varMap {
return value
}
return emptyInterface
}
// returns map of variables as keys and variable values as values
func getValues(ctx context.EvalInterface, groups [][]string) map[string]interface{} {
var emptyInterface interface{}
subs := map[string]interface{}{}
for _, group := range groups {
if len(group) == 2 {
// 0th is string
varName := group[0]
varValue := group[1]
variable, err := ctx.Query(varValue)
if err != nil {
glog.V(4).Infof("variable substitution failed for query %s: %v", varName, err)
subs[varName] = emptyInterface
continue
}
if variable == nil {
subs[varName] = emptyInterface
} else {
subs[varName] = variable
}
}
}
return subs
}
func isAllVarStrings(subVar map[string]interface{}) bool {
for _, value := range subVar {
if _, ok := value.(string); !ok {
return false
}
}
return true
}
func getOperator(pattern string) string {
operatorVariable := operator.GetOperatorFromStringPattern(pattern)
if operatorVariable == operator.Equal {
return ""
}
return string(operatorVariable)
}

View file

@ -1,80 +0,0 @@
package variables
import (
"fmt"
"regexp"
"strconv"
)
//CheckVariables checks if the variable regex has been used
func CheckVariables(pattern interface{}, variables []string, path string) error {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return checkMap(typedPattern, variables, path)
case []interface{}:
return checkArray(typedPattern, variables, path)
case string:
return checkValue(typedPattern, variables, path)
default:
return nil
}
}
func checkMap(patternMap map[string]interface{}, variables []string, path string) error {
for patternKey, patternElement := range patternMap {
if err := CheckVariables(patternElement, variables, path+patternKey+"/"); err != nil {
return err
}
}
return nil
}
func checkArray(patternList []interface{}, variables []string, path string) error {
for idx, patternElement := range patternList {
if err := CheckVariables(patternElement, variables, path+strconv.Itoa(idx)+"/"); err != nil {
return err
}
}
return nil
}
func checkValue(valuePattern string, variables []string, path string) error {
operatorVariable := getOperator(valuePattern)
variable := valuePattern[len(operatorVariable):]
if checkValueVariable(variable, variables) {
return fmt.Errorf(path + valuePattern)
}
return nil
}
func checkValueVariable(valuePattern string, variables []string) bool {
variableRegex := regexp.MustCompile(variableRegex)
groups := variableRegex.FindAllStringSubmatch(valuePattern, -1)
if len(groups) == 0 {
// no variables
return false
}
// if variables are defined, check against the list of variables to be filtered
for _, group := range groups {
if len(group) == 2 {
// group[0] -> {{variable}}
// group[1] -> variable
if variablePatternSearch(group[1], variables) {
return true
}
}
}
return false
}
func variablePatternSearch(pattern string, regexs []string) bool {
for _, regex := range regexs {
varRegex := regexp.MustCompile(regex)
found := varRegex.FindString(pattern)
if found != "" {
return true
}
}
return true
}

View file

@ -58,12 +58,16 @@ func Test_variablesub1(t *testing.T) {
resultMap := []byte(`{"data":{"rules":[{"apiGroups":[""],"resourceNames":["temp"],"resources":["namespaces"],"verbs":["*"]}]},"kind":"ClusterRole","name":"ns-owner-user1"}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
t.Error(err)
@ -80,12 +84,15 @@ func Test_variablesub1(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(resultMap, resultRaw) {
if !reflect.DeepEqual(resultRaw, resultMap) {
t.Log(string(resultMap))
t.Log(string(resultRaw))
t.Error("result does not match")
@ -139,12 +146,16 @@ func Test_variablesub_multiple(t *testing.T) {
resultMap := []byte(`{"data":{"rules":[{"apiGroups":[""],"resourceNames":["temp"],"resources":["namespaces"],"verbs":["*"]}]},"kind":"ClusterRole","name":"ns-owner-n1-user1-bindings"}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -163,11 +174,14 @@ func Test_variablesub_multiple(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Log(string(resultMap))
t.Log(string(resultRaw))
@ -219,12 +233,16 @@ func Test_variablesubstitution(t *testing.T) {
Username: "user1",
},
}
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -243,8 +261,10 @@ func Test_variablesubstitution(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
@ -279,12 +299,16 @@ func Test_variableSubstitutionValue(t *testing.T) {
resultMap := []byte(`{"spec":{"name":"temp"}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -298,8 +322,10 @@ func Test_variableSubstitutionValue(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
@ -331,12 +357,16 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
`)
resultMap := []byte(`{"spec":{"name":"!temp"}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -350,14 +380,16 @@ func Test_variableSubstitutionValueOperatorNotEqual(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
t.Log(string(resultRaw))
t.Log(string(resultMap))
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Log(string(resultRaw))
t.Log(string(resultMap))
t.Error("result does not match")
}
}
@ -383,14 +415,17 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
}
}
`)
resultMap := []byte(`{"spec":{"name":null}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -404,16 +439,11 @@ func Test_variableSubstitutionValueFail(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if err != nil {
t.Error(err)
}
t.Log(string(resultRaw))
t.Log(string(resultMap))
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Error("result does not match")
if _, err := SubstituteVars(ctx, patternCopy); err == nil {
t.Log("expected to fails")
t.Fail()
}
}
func Test_variableSubstitutionObject(t *testing.T) {
@ -444,12 +474,16 @@ func Test_variableSubstitutionObject(t *testing.T) {
`)
resultMap := []byte(`{"spec":{"variable":{"var1":"temp1","var2":"temp2","varNested":{"var1":"temp1"}}}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -463,14 +497,16 @@ func Test_variableSubstitutionObject(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
t.Log(string(resultRaw))
t.Log(string(resultMap))
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Log(string(resultRaw))
t.Log(string(resultMap))
t.Error("result does not match")
}
}
@ -504,12 +540,16 @@ func Test_variableSubstitutionObjectOperatorNotEqualFail(t *testing.T) {
resultMap := []byte(`{"spec":{"variable":null}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -523,14 +563,16 @@ func Test_variableSubstitutionObjectOperatorNotEqualFail(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
t.Log(string(resultRaw))
t.Log(string(resultMap))
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Log(string(resultRaw))
t.Log(string(resultMap))
t.Error("result does not match")
}
}
@ -565,12 +607,16 @@ func Test_variableSubstitutionMultipleObject(t *testing.T) {
resultMap := []byte(`{"spec":{"var":"temp1","variable":{"var1":"temp1","var2":"temp2","varNested":{"var1":"temp1"}}}}`)
var pattern, resource interface{}
var pattern, patternCopy, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(patternMap, &patternCopy)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
@ -584,14 +630,16 @@ func Test_variableSubstitutionMultipleObject(t *testing.T) {
t.Error(err)
}
value := SubstituteVariables(ctx, pattern)
resultRaw, err := json.Marshal(value)
if _, err := SubstituteVars(ctx, patternCopy); err != nil {
t.Error(err)
}
resultRaw, err := json.Marshal(patternCopy)
if err != nil {
t.Error(err)
}
t.Log(string(resultRaw))
t.Log(string(resultMap))
if !reflect.DeepEqual(resultMap, resultRaw) {
t.Log(string(resultRaw))
t.Log(string(resultMap))
t.Error("result does not match")
}
}

View file

@ -0,0 +1,175 @@
package variables
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/operator"
)
const variableRegex = `\{\{([^{}]*)\}\}`
//SubstituteVars replaces the variables with the values defined in the context
// - if any variable is invaid or has nil value, it is considered as a failed varable substitution
func SubstituteVars(ctx context.EvalInterface, pattern interface{}) (interface{}, error) {
errs := []error{}
pattern = subVars(ctx, pattern, "", &errs)
if len(errs) == 0 {
// no error while parsing the pattern
return pattern, nil
}
return pattern, fmt.Errorf("variable(s) not found or has nil values: %v", errs)
}
func subVars(ctx context.EvalInterface, pattern interface{}, path string, errs *[]error) interface{} {
switch typedPattern := pattern.(type) {
case map[string]interface{}:
return subMap(ctx, typedPattern, path, errs)
case []interface{}:
return subArray(ctx, typedPattern, path, errs)
case string:
return subVal(ctx, typedPattern, path, errs)
default:
return pattern
}
}
func subMap(ctx context.EvalInterface, patternMap map[string]interface{}, path string, errs *[]error) map[string]interface{} {
for key, patternElement := range patternMap {
curPath := path + "/" + key
value := subVars(ctx, patternElement, curPath, errs)
patternMap[key] = value
}
return patternMap
}
func subArray(ctx context.EvalInterface, patternList []interface{}, path string, errs *[]error) []interface{} {
for idx, patternElement := range patternList {
curPath := path + "/" + strconv.Itoa(idx)
value := subVars(ctx, patternElement, curPath, errs)
patternList[idx] = value
}
return patternList
}
func subVal(ctx context.EvalInterface, valuePattern interface{}, path string, errs *[]error) interface{} {
var emptyInterface interface{}
valueStr, ok := valuePattern.(string)
if !ok {
glog.Infof("failed to convert %v to string", valuePattern)
return emptyInterface
}
operatorVariable := getOp(valueStr)
variable := valueStr[len(operatorVariable):]
// substitute variable with value
value, failedVars := getValQuery(ctx, variable)
// if there are failedVars at this level
// capture as error and the path to the variables
for _, failedVar := range failedVars {
failedPath := path + "/" + failedVar
*errs = append(*errs, NewInvalidPath(failedPath))
}
if operatorVariable == "" {
// default or operator.Equal
// equal + string value
// object variable
return value
}
// operator + string variable
switch typedValue := value.(type) {
case string:
return string(operatorVariable) + typedValue
default:
glog.Infof("cannot use operator with object variables. operator used %s in pattern %v", string(operatorVariable), valuePattern)
return emptyInterface
}
}
func getOp(pattern string) string {
operatorVariable := operator.GetOperatorFromStringPattern(pattern)
if operatorVariable == operator.Equal {
return ""
}
return string(operatorVariable)
}
func getValQuery(ctx context.EvalInterface, valuePattern string) (interface{}, []string) {
var emptyInterface interface{}
validRegex := regexp.MustCompile(variableRegex)
groups := validRegex.FindAllStringSubmatch(valuePattern, -1)
// there can be multiple varialbes in a single value pattern
varMap, failedVars := getVal(ctx, groups)
if len(varMap) == 0 && len(failedVars) == 0 {
// no variables
// return original value
return valuePattern, nil
}
if isAllStrings(varMap) {
newVal := valuePattern
for key, value := range varMap {
if val, ok := value.(string); ok {
newVal = strings.Replace(newVal, key, val, -1)
}
}
return newVal, failedVars
}
// multiple substitution per statement for non-string types are not supported
for _, value := range varMap {
return value, failedVars
}
return emptyInterface, failedVars
}
func getVal(ctx context.EvalInterface, groups [][]string) (map[string]interface{}, []string) {
substiutions := map[string]interface{}{}
var failedVars []string
for _, group := range groups {
// 0th is the string
varName := group[0]
varValue := group[1]
variable, err := ctx.Query(varValue)
// err !=nil -> invalid expression
// err == nil && variable == nil -> variable is empty or path is not present
// a variable with empty value is considered as a failed variable
if err != nil || (err == nil && variable == nil) {
// could not find the variable at the given path
failedVars = append(failedVars, varName)
continue
}
substiutions[varName] = variable
}
return substiutions, failedVars
}
func isAllStrings(subVar map[string]interface{}) bool {
if len(subVar) == 0 {
return false
}
for _, value := range subVar {
if _, ok := value.(string); !ok {
return false
}
}
return true
}
//InvalidPath stores the path to failed variable
type InvalidPath struct {
path string
}
func (e *InvalidPath) Error() string {
return e.path
}
//NewInvalidPath returns a new Invalid Path error
func NewInvalidPath(path string) *InvalidPath {
return &InvalidPath{path: path}
}

View file

@ -0,0 +1,130 @@
package variables
import (
"encoding/json"
"testing"
"github.com/nirmata/kyverno/pkg/engine/context"
)
func Test_subVars_success(t *testing.T) {
patternMap := []byte(`
{
"kind": "{{request.object.metadata.name}}",
"name": "ns-owner-{{request.object.metadata.name}}",
"data": {
"rules": [
{
"apiGroups": [
"{{request.object.metadata.name}}"
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name}}"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
var pattern, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
t.Error(err)
}
// context
ctx := context.NewContext()
err = ctx.AddResource(resourceRaw)
if err != nil {
t.Error(err)
}
if _, err := SubstituteVars(ctx, pattern); err != nil {
t.Error(err)
}
}
func Test_subVars_failed(t *testing.T) {
patternMap := []byte(`
{
"kind": "{{request.object.metadata.name1}}",
"name": "ns-owner-{{request.object.metadata.name}}",
"data": {
"rules": [
{
"apiGroups": [
"{{request.object.metadata.name}}"
],
"resources": [
"namespaces"
],
"verbs": [
"*"
],
"resourceNames": [
"{{request.object.metadata.name1}}"
]
}
]
}
}
`)
resourceRaw := []byte(`
{
"metadata": {
"name": "temp",
"namespace": "n1"
},
"spec": {
"namespace": "n1",
"name": "temp1"
}
}
`)
var pattern, resource interface{}
var err error
err = json.Unmarshal(patternMap, &pattern)
if err != nil {
t.Error(err)
}
err = json.Unmarshal(resourceRaw, &resource)
if err != nil {
t.Error(err)
}
// context
ctx := context.NewContext()
err = ctx.AddResource(resourceRaw)
if err != nil {
t.Error(err)
}
if _, err := SubstituteVars(ctx, pattern); err == nil {
t.Error("error is expected")
}
}

View file

@ -1,42 +1,24 @@
package cleanup
import (
"time"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
dclient "github.com/nirmata/kyverno/pkg/dclient"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
const timoutMins = 2
const timeout = time.Minute * timoutMins // 2 minutes
func (c *Controller) processGR(gr kyverno.GenerateRequest) error {
// 1-Corresponding policy has been deleted
_, err := c.pLister.Get(gr.Spec.Policy)
if errors.IsNotFound(err) {
glog.V(4).Infof("delete GR %s", gr.Name)
return c.control.Delete(gr.Name)
}
// 1- Corresponding policy has been deleted
// then we dont delete the generated resources
// 2- Check for elapsed time since update
if gr.Status.State == kyverno.Completed {
glog.V(4).Infof("checking if owner exists for gr %s", gr.Name)
if !ownerResourceExists(c.client, gr) {
if err := deleteGeneratedResources(c.client, gr); err != nil {
return err
}
glog.V(4).Infof("delete GR %s", gr.Name)
return c.control.Delete(gr.Name)
// 2- The trigger resource is deleted, then delete the generated resources
if !ownerResourceExists(c.client, gr) {
if err := deleteGeneratedResources(c.client, gr); err != nil {
return err
}
return nil
}
createTime := gr.GetCreationTimestamp()
if time.Since(createTime.UTC()) > timeout {
// the GR was in state ["",Failed] for more than timeout
glog.V(4).Infof("GR %s was not processed successfully in %d minutes", gr.Name, timoutMins)
glog.V(4).Infof("delete GR %s", gr.Name)
// - trigger-resource is deleted
// - generated-resources are deleted
// - > Now delete the GenerateRequest CR
return c.control.Delete(gr.Name)
}
return nil
@ -44,16 +26,22 @@ func (c *Controller) processGR(gr kyverno.GenerateRequest) error {
func ownerResourceExists(client *dclient.Client, gr kyverno.GenerateRequest) bool {
_, err := client.GetResource(gr.Spec.Resource.Kind, gr.Spec.Resource.Namespace, gr.Spec.Resource.Name)
if err != nil {
// trigger resources has been deleted
if apierrors.IsNotFound(err) {
return false
}
if err != nil {
glog.V(4).Infof("Failed to get resource %s/%s/%s: error : %s", gr.Spec.Resource.Kind, gr.Spec.Resource.Namespace, gr.Spec.Resource.Name, err)
}
// if there was an error while querying the resources we dont delete the generated resources
// but expect the deletion in next reconciliation loop
return true
}
func deleteGeneratedResources(client *dclient.Client, gr kyverno.GenerateRequest) error {
for _, genResource := range gr.Status.GeneratedResources {
err := client.DeleteResource(genResource.Kind, genResource.Namespace, genResource.Name, false)
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
glog.V(4).Infof("resource %s/%s/%s not found, will no delete", genResource.Kind, genResource.Namespace, genResource.Name)
continue
}

View file

@ -1,9 +1,8 @@
package generate
import (
"errors"
"encoding/json"
"fmt"
"reflect"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
@ -12,10 +11,8 @@ import (
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/validate"
"github.com/nirmata/kyverno/pkg/engine/variables"
"github.com/nirmata/kyverno/pkg/policyviolation"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func (c *Controller) processGR(gr *kyverno.GenerateRequest) error {
@ -29,24 +26,10 @@ func (c *Controller) processGR(gr *kyverno.GenerateRequest) error {
glog.V(4).Infof("resource does not exist or is yet to be created, requeuing: %v", err)
return err
}
// 2 - Apply the generate policy on the resource
genResources, err = c.applyGenerate(*resource, *gr)
switch e := err.(type) {
case *Violation:
// Generate event
// - resource -> rule failed and created PV
// - policy -> failed to apply of resource and created PV
c.pvGenerator.Add(generatePV(*gr, *resource, e))
default:
// Generate event
// - resource -> rule failed
// - policy -> failed tp apply on resource
glog.V(4).Info(e)
}
// 3 - Report Events
reportEvents(err, c.eventGen, *gr, *resource)
// 4 - Update Status
return updateStatus(c.statusControl, *gr, err, genResources)
}
@ -96,15 +79,8 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
return nil, fmt.Errorf("policy %s, dont not apply to resource %v", gr.Spec.Policy, gr.Spec.Resource)
}
if pv := buildPathNotPresentPV(engineResponse); pv != nil {
c.pvGenerator.Add(pv...)
// variable substitiution fails in ruleInfo (match,exclude,condition)
// the overall policy should not apply to resource
return nil, fmt.Errorf("referenced path not present in generate policy %s", policy.Name)
}
// Apply the generate rule on resource
return applyGeneratePolicy(c.client, policyContext, gr.Status.State)
return applyGeneratePolicy(c.client, policyContext)
}
func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateRequest, err error, genResources []kyverno.ResourceSpec) error {
@ -116,7 +92,7 @@ func updateStatus(statusControl StatusControlInterface, gr kyverno.GenerateReque
return statusControl.Success(gr, genResources)
}
func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyContext, state kyverno.GenerateRequestState) ([]kyverno.ResourceSpec, error) {
func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyContext) ([]kyverno.ResourceSpec, error) {
// List of generatedResources
var genResources []kyverno.ResourceSpec
// Get the response as the actions to be performed on the resource
@ -135,7 +111,7 @@ func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyCont
if !rule.HasGenerate() {
continue
}
genResource, err := applyRule(client, rule, resource, ctx, state, processExisting)
genResource, err := applyRule(client, rule, resource, ctx, processExisting)
if err != nil {
return nil, err
}
@ -145,65 +121,64 @@ func applyGeneratePolicy(client *dclient.Client, policyContext engine.PolicyCont
return genResources, nil
}
func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState, processExisting bool) (kyverno.ResourceSpec, error) {
func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.Unstructured, ctx context.EvalInterface, processExisting bool) (kyverno.ResourceSpec, error) {
var rdata map[string]interface{}
var err error
var mode ResourceMode
var noGenResource kyverno.ResourceSpec
if invalidPaths := variables.ValidateVariables(ctx, rule.Generation.ResourceSpec); len(invalidPaths) != 0 {
return noGenResource, NewViolation(rule.Name, fmt.Errorf("path not present in generate resource spec: %s", invalidPaths))
// convert to unstructured Resource
genUnst, err := getUnstrRule(rule.Generation.DeepCopy())
if err != nil {
return noGenResource, err
}
// Variable substitutions
// format : {{<variable_name}}
// - if there is variables that are not defined the context -> results in error and rule is not applied
// - valid variables are replaced with the values
if _, err := variables.SubstituteVars(ctx, genUnst.Object); err != nil {
return noGenResource, err
}
genKind, _, err := unstructured.NestedString(genUnst.Object, "kind")
if err != nil {
return noGenResource, err
}
genName, _, err := unstructured.NestedString(genUnst.Object, "name")
if err != nil {
return noGenResource, err
}
genNamespace, _, err := unstructured.NestedString(genUnst.Object, "namespace")
if err != nil {
return noGenResource, err
}
// variable substitution
// - name
// - namespace
// - clone.name
// - clone.namespace
gen := variableSubsitutionForAttributes(rule.Generation, ctx)
// Resource to be generated
newGenResource := kyverno.ResourceSpec{
Kind: gen.Kind,
Namespace: gen.Namespace,
Name: gen.Name,
Kind: genKind,
Namespace: genNamespace,
Name: genName,
}
genData, _, err := unstructured.NestedMap(genUnst.Object, "data")
if err != nil {
return noGenResource, err
}
genCopy, _, err := unstructured.NestedMap(genUnst.Object, "clone")
if err != nil {
return noGenResource, err
}
// DATA
if gen.Data != nil {
if rdata, err = handleData(rule.Name, gen, client, resource, ctx, state); err != nil {
glog.V(4).Info(err)
switch e := err.(type) {
case *ParseFailed, *NotFound, *ConfigNotFound:
// handled errors
case *Violation:
// create policy violation
return noGenResource, e
default:
// errors that cant be handled
return noGenResource, e
}
}
if rdata == nil {
// existing resource contains the configuration
return newGenResource, nil
}
if genData != nil {
rdata, mode, err = manageData(genKind, genNamespace, genName, genData, client, resource)
} else {
rdata, mode, err = manageClone(genKind, genNamespace, genName, genCopy, client, resource)
}
// CLONE
if gen.Clone != (kyverno.CloneFrom{}) {
if rdata, err = handleClone(rule.Name, gen, client, resource, ctx, state); err != nil {
glog.V(4).Info(err)
switch e := err.(type) {
case *NotFound:
// handled errors
return noGenResource, e
default:
// errors that cant be handled
return noGenResource, e
}
}
if rdata == nil {
// resource already exists
return newGenResource, nil
}
if err != nil {
return noGenResource, err
}
if rdata == nil {
// existing resource contains the configuration
return newGenResource, nil
}
if processExisting {
// handle existing resources
@ -211,153 +186,141 @@ func applyRule(client *dclient.Client, rule kyverno.Rule, resource unstructured.
// we do not create new resource
return noGenResource, err
}
// Create the generate resource
// build the resource template
newResource := &unstructured.Unstructured{}
newResource.SetUnstructuredContent(rdata)
newResource.SetName(gen.Name)
newResource.SetNamespace(gen.Namespace)
// Reset resource version
newResource.SetResourceVersion("")
newResource.SetName(genName)
newResource.SetNamespace(genNamespace)
glog.V(4).Infof("creating resource %v", newResource)
_, err = client.CreateResource(gen.Kind, gen.Namespace, newResource, false)
if err != nil {
glog.Info(err)
return noGenResource, err
// manage labels
// - app.kubernetes.io/managed-by: kyverno
// - kyverno.io/generated-by: kind/namespace/name (trigger resource)
manageLabels(newResource, resource)
if mode == Create {
// Reset resource version
newResource.SetResourceVersion("")
// Create the resource
glog.V(4).Infof("Creating new resource %s/%s/%s", genKind, genNamespace, genName)
_, err = client.CreateResource(genKind, genNamespace, newResource, false)
if err != nil {
// Failed to create resource
return noGenResource, err
}
glog.V(4).Infof("Created new resource %s/%s/%s", genKind, genNamespace, genName)
} else if mode == Update {
glog.V(4).Infof("Updating existing resource %s/%s/%s", genKind, genNamespace, genName)
// Update the resource
_, err := client.UpdateResource(genKind, genNamespace, newResource, false)
if err != nil {
// Failed to update resource
return noGenResource, err
}
glog.V(4).Infof("Updated existing resource %s/%s/%s", genKind, genNamespace, genName)
}
glog.V(4).Infof("created new resource %s %s %s ", gen.Kind, gen.Namespace, gen.Name)
return newGenResource, nil
}
func variableSubsitutionForAttributes(gen kyverno.Generation, ctx context.EvalInterface) kyverno.Generation {
// Name
name := gen.Name
namespace := gen.Namespace
newNameVar := variables.SubstituteVariables(ctx, name)
if newName, ok := newNameVar.(string); ok {
gen.Name = newName
}
newNamespaceVar := variables.SubstituteVariables(ctx, namespace)
if newNamespace, ok := newNamespaceVar.(string); ok {
gen.Namespace = newNamespace
}
if gen.Clone != (kyverno.CloneFrom{}) {
// Clone
cloneName := gen.Clone.Name
cloneNamespace := gen.Clone.Namespace
newcloneNameVar := variables.SubstituteVariables(ctx, cloneName)
if newcloneName, ok := newcloneNameVar.(string); ok {
gen.Clone.Name = newcloneName
}
newcloneNamespaceVar := variables.SubstituteVariables(ctx, cloneNamespace)
if newcloneNamespace, ok := newcloneNamespaceVar.(string); ok {
gen.Clone.Namespace = newcloneNamespace
}
}
return gen
}
func handleData(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Data); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate data: %s", invalidPaths))
}
//work on copy
copyDataTemp := reflect.Indirect(reflect.ValueOf(generateRule.Data))
copyData := copyDataTemp.Interface()
newData := variables.SubstituteVariables(ctx, copyData)
// check if resource exists
obj, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
glog.V(4).Info(err)
func manageData(kind, namespace, name string, data map[string]interface{}, client *dclient.Client, resource unstructured.Unstructured) (map[string]interface{}, ResourceMode, error) {
// check if resource to be generated exists
obj, err := client.GetResource(kind, namespace, name)
if apierrors.IsNotFound(err) {
glog.V(4).Info(string(state))
// Resource does not exist
if state == "" {
// Processing the request first time
rdata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&newData)
glog.V(4).Info(err)
if err != nil {
return nil, NewParseFailed(newData, err)
}
return rdata, nil
}
glog.V(4).Info("Creating violation")
// State : Failed,Completed
// request has been processed before, so dont create the resource
// report Violation to notify the error
return nil, NewViolation(ruleName, NewNotFound(generateRule.Kind, generateRule.Namespace, generateRule.Name))
glog.V(4).Infof("Resource %s/%s/%s does not exists, will try to create", kind, namespace, name)
return data, Create, nil
}
if err != nil {
//something wrong while fetching resource
return nil, err
// client-errors
return nil, Skip, err
}
// Resource exists; verfiy the content of the resource
ok, err := checkResource(ctx, newData, obj)
if err != nil {
//something wrong with configuration
glog.V(4).Info(err)
return nil, err
err = checkResource(data, obj)
if err == nil {
// Existing resource does contain the mentioned configuration in spec, skip processing the resource as it is already in expected state
return nil, Skip, nil
}
if !ok {
return nil, NewConfigNotFound(newData, generateRule.Kind, generateRule.Namespace, generateRule.Name)
}
// Existing resource does contain the required
return nil, nil
glog.V(4).Infof("Resource %s/%s/%s exists but missing required configuration, will try to update", kind, namespace, name)
return data, Update, nil
}
func handleClone(ruleName string, generateRule kyverno.Generation, client *dclient.Client, resource unstructured.Unstructured, ctx context.EvalInterface, state kyverno.GenerateRequestState) (map[string]interface{}, error) {
if invalidPaths := variables.ValidateVariables(ctx, generateRule.Clone); len(invalidPaths) != 0 {
return nil, NewViolation(ruleName, fmt.Errorf("path not present in generate clone: %s", invalidPaths))
}
// check if resource exists
_, err := client.GetResource(generateRule.Kind, generateRule.Namespace, generateRule.Name)
func manageClone(kind, namespace, name string, clone map[string]interface{}, client *dclient.Client, resource unstructured.Unstructured) (map[string]interface{}, ResourceMode, error) {
// check if resource to be generated exists
_, err := client.GetResource(kind, namespace, name)
if err == nil {
// resource exists
return nil, nil
// resource does exists, not need to process further as it is already in expected state
return nil, Skip, nil
}
//TODO: check this
if !apierrors.IsNotFound(err) {
//something wrong while fetching resource
return nil, err
return nil, Skip, err
}
// get reference clone resource
obj, err := client.GetResource(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
if apierrors.IsNotFound(err) {
return nil, NewNotFound(generateRule.Kind, generateRule.Clone.Namespace, generateRule.Clone.Name)
}
newRNs, _, err := unstructured.NestedString(clone, "namespace")
if err != nil {
return nil, Skip, err
}
newRName, _, err := unstructured.NestedString(clone, "name")
if err != nil {
return nil, Skip, err
}
// Short-circuit if the resource to be generated and the clone is the same
if newRNs == namespace && newRName == name {
// attempting to clone it self, this will fail -> short-ciruit it
return nil, Skip, nil
}
glog.V(4).Infof("check if resource %s/%s/%s exists", kind, newRNs, newRName)
// check if the resource as reference in clone exists?
obj, err := client.GetResource(kind, newRNs, newRName)
if err != nil {
return nil, Skip, fmt.Errorf("reference clone resource %s/%s/%s not found. %v", kind, newRNs, newRName, err)
}
// create the resource based on the reference clone
return obj.UnstructuredContent(), Create, nil
}
// ResourceMode defines the mode for generated resource
type ResourceMode string
const (
//Skip : failed to process rule, will not update the resource
Skip ResourceMode = "SKIP"
//Create : create a new resource
Create = "CREATE"
//Update : update/overwrite the new resource
Update = "UPDATE"
)
func checkResource(newResourceSpec interface{}, resource *unstructured.Unstructured) error {
// check if the resource spec if a subset of the resource
if path, err := validate.ValidateResourceWithPattern(resource.Object, newResourceSpec); err != nil {
glog.V(4).Infof("Failed to match the resource at path %s: err %v", path, err)
return err
}
return nil
}
func getUnstrRule(rule *kyverno.Generation) (*unstructured.Unstructured, error) {
ruleData, err := json.Marshal(rule)
if err != nil {
//something wrong while fetching resource
return nil, err
}
return obj.UnstructuredContent(), nil
return ConvertToUnstructured(ruleData)
}
func checkResource(ctx context.EvalInterface, newResourceSpec interface{}, resource *unstructured.Unstructured) (bool, error) {
// check if the resource spec if a subset of the resource
path, err := validate.ValidateResourceWithPattern(ctx, resource.Object, newResourceSpec)
if !reflect.DeepEqual(err, validate.ValidationError{}) {
glog.V(4).Infof("config not a subset of resource. failed at path %s: %v", path, err)
return false, errors.New(err.ErrorMsg)
//ConvertToUnstructured converts the resource to unstructured format
func ConvertToUnstructured(data []byte) (*unstructured.Unstructured, error) {
resource := &unstructured.Unstructured{}
err := resource.UnmarshalJSON(data)
if err != nil {
return nil, err
}
return true, nil
}
func generatePV(gr kyverno.GenerateRequest, resource unstructured.Unstructured, err *Violation) policyviolation.Info {
info := policyviolation.Info{
PolicyName: gr.Spec.Policy,
Resource: resource,
Rules: []kyverno.ViolatedRule{{
Name: err.rule,
Type: "Generation",
Message: err.Error(),
}},
}
return info
return resource, nil
}

57
pkg/generate/labels.go Normal file
View file

@ -0,0 +1,57 @@
package generate
import (
"fmt"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func manageLabels(unstr *unstructured.Unstructured, triggerResource unstructured.Unstructured) {
// add managedBY label if not defined
labels := unstr.GetLabels()
if labels == nil {
labels = map[string]string{}
}
// handle managedBy label
managedBy(labels)
// handle generatedBy label
generatedBy(labels, triggerResource)
// update the labels
unstr.SetLabels(labels)
}
func managedBy(labels map[string]string) {
// ManagedBy label
key := "app.kubernetes.io/managed-by"
value := "kyverno"
val, ok := labels[key]
if ok {
if val != value {
glog.Infof("resource managed by %s, kyverno wont over-ride the label", val)
return
}
}
if !ok {
// add label
labels[key] = value
}
}
func generatedBy(labels map[string]string, triggerResource unstructured.Unstructured) {
key := "kyverno.io/generated-by"
value := fmt.Sprintf("%s-%s-%s", triggerResource.GetKind(), triggerResource.GetNamespace(), triggerResource.GetName())
val, ok := labels[key]
if ok {
if val != value {
glog.Infof("resource generated by %s, kyverno wont over-ride the label", val)
return
}
}
if !ok {
// add label
labels[key] = value
}
}

View file

@ -5,9 +5,7 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@ -20,45 +18,9 @@ func reportEvents(err error, eventGen event.Interface, gr kyverno.GenerateReques
eventGen.Add(events...)
return
}
switch e := err.(type) {
case *Violation:
// - resource -> rule failed and created PV
// - policy -> failed to apply of resource and created PV
glog.V(4).Infof("reporing events for %v", e)
events := failedEventsPV(err, gr, resource)
eventGen.Add(events...)
default:
// - resource -> rule failed
// - policy -> failed tp apply on resource
glog.V(4).Infof("reporing events for %v", e)
events := failedEvents(err, gr, resource)
eventGen.Add(events...)
}
}
func failedEventsPV(err error, gr kyverno.GenerateRequest, resource unstructured.Unstructured) []event.Info {
var events []event.Info
// Cluster Policy
pe := event.Info{}
pe.Kind = "ClusterPolicy"
// cluserwide-resource
pe.Name = gr.Spec.Policy
pe.Reason = event.PolicyViolation.String()
pe.Source = event.GeneratePolicyController
pe.Message = fmt.Sprintf("policy failed to apply on resource %s/%s/%s creating violation: %v", resource.GetKind(), resource.GetNamespace(), resource.GetName(), err)
events = append(events, pe)
// Resource
re := event.Info{}
re.Kind = resource.GetKind()
re.Namespace = resource.GetNamespace()
re.Name = resource.GetName()
re.Reason = event.PolicyViolation.String()
re.Source = event.GeneratePolicyController
re.Message = fmt.Sprintf("policy %s failed to apply created violation: %v", gr.Spec.Policy, err)
events = append(events, re)
return events
glog.V(4).Infof("reporing events for %v", err)
events := failedEvents(err, gr, resource)
eventGen.Add(events...)
}
func failedEvents(err error, gr kyverno.GenerateRequest, resource unstructured.Unstructured) []event.Info {
@ -110,13 +72,3 @@ func successEvents(gr kyverno.GenerateRequest, resource unstructured.Unstructure
return events
}
// buildPathNotPresentPV build violation info when referenced path not found
func buildPathNotPresentPV(er response.EngineResponse) []policyviolation.Info {
for _, rr := range er.PolicyResponse.Rules {
if rr.PathNotPresent {
return policyviolation.GeneratePVsFromEngineResponse([]response.EngineResponse{er})
}
}
return nil
}

View file

@ -1,251 +0,0 @@
package apply
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
yaml "k8s.io/apimachinery/pkg/util/yaml"
memory "k8s.io/client-go/discovery/cached/memory"
dynamic "k8s.io/client-go/dynamic"
kubernetes "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
)
const (
applyExample = ` # Apply a policy to the resource.
kyverno apply @policy.yaml @resource.yaml
kyverno apply @policy.yaml @resourceDir/
kyverno apply @policy.yaml @resource.yaml --kubeconfig=$PATH_TO_KUBECONFIG_FILE`
defaultYamlSeparator = "---"
)
// NewCmdApply returns the apply command for kyverno
func NewCmdApply(in io.Reader, out, errout io.Writer) *cobra.Command {
var kubeconfig string
cmd := &cobra.Command{
Use: "apply",
Short: "Apply policy on the resource(s)",
Example: applyExample,
Run: func(cmd *cobra.Command, args []string) {
policy, resources := complete(kubeconfig, args)
output := applyPolicy(policy, resources)
fmt.Printf("%v\n", output)
},
}
cmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file")
return cmd
}
func complete(kubeconfig string, args []string) (*kyverno.ClusterPolicy, []*resourceInfo) {
policyDir, resourceDir, err := validateDir(args)
if err != nil {
glog.Errorf("Failed to parse file path, err: %v\n", err)
os.Exit(1)
}
// extract policy
policy, err := extractPolicy(policyDir)
if err != nil {
glog.Errorf("Failed to extract policy: %v\n", err)
os.Exit(1)
}
// extract rawResource
resources, err := extractResource(resourceDir, kubeconfig)
if err != nil {
glog.Errorf("Failed to parse resource: %v", err)
os.Exit(1)
}
return policy, resources
}
func applyPolicy(policy *kyverno.ClusterPolicy, resources []*resourceInfo) (output string) {
for _, resource := range resources {
patchedDocument, err := applyPolicyOnRaw(policy, resource.rawResource, resource.gvk)
if err != nil {
glog.Errorf("Error applying policy on resource %s, err: %v\n", resource.gvk.Kind, err)
continue
}
out, err := prettyPrint(patchedDocument)
if err != nil {
glog.Errorf("JSON parse error: %v\n", err)
continue
}
output = output + fmt.Sprintf("---\n%s", string(out))
}
return
}
func applyPolicyOnRaw(policy *kyverno.ClusterPolicy, rawResource []byte, gvk *metav1.GroupVersionKind) ([]byte, error) {
patchedResource := rawResource
var err error
rname := engine.ParseNameFromObject(rawResource)
rns := engine.ParseNamespaceFromObject(rawResource)
resource, err := ConvertToUnstructured(rawResource)
if err != nil {
return nil, err
}
//TODO check if the kind information is present resource
// Process Mutation
engineResponse := engine.Mutate(engine.PolicyContext{Policy: *policy, NewResource: *resource})
if !engineResponse.IsSuccesful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range engineResponse.PolicyResponse.Rules {
glog.Warning(r.Message)
}
} else if len(engineResponse.PolicyResponse.Rules) > 0 {
glog.Infof("Mutation from policy %s has applied successfully to %s %s/%s", policy.Name, gvk.Kind, rname, rns)
// Process Validation
engineResponse := engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: *resource})
if !engineResponse.IsSuccesful() {
glog.Infof("Failed to apply policy %s on resource %s/%s", policy.Name, rname, rns)
for _, r := range engineResponse.PolicyResponse.Rules {
glog.Warning(r.Message)
}
return patchedResource, fmt.Errorf("policy %s on resource %s/%s not satisfied", policy.Name, rname, rns)
} else if len(engineResponse.PolicyResponse.Rules) > 0 {
glog.Infof("Validation from policy %s has applied successfully to %s %s/%s", policy.Name, gvk.Kind, rname, rns)
}
}
return patchedResource, nil
}
func extractPolicy(fileDir string) (*kyverno.ClusterPolicy, error) {
policy := &kyverno.ClusterPolicy{}
file, err := loadFile(fileDir)
if err != nil {
return nil, fmt.Errorf("failed to load file: %v", err)
}
policyBytes, err := yaml.ToJSON(file)
if err != nil {
return nil, err
}
if err := json.Unmarshal(policyBytes, policy); err != nil {
return nil, fmt.Errorf("failed to decode policy %s, err: %v", policy.Name, err)
}
if policy.TypeMeta.Kind != "ClusterPolicy" {
return nil, fmt.Errorf("failed to parse policy")
}
return policy, nil
}
type resourceInfo struct {
rawResource []byte
gvk *metav1.GroupVersionKind
}
func extractResource(fileDir, kubeconfig string) ([]*resourceInfo, error) {
var files []string
var resources []*resourceInfo
// check if applied on multiple resources
isDir, err := isDir(fileDir)
if err != nil {
return nil, err
}
if isDir {
files, err = scanDir(fileDir)
if err != nil {
return nil, err
}
} else {
files = []string{fileDir}
}
for _, dir := range files {
data, err := loadFile(dir)
if err != nil {
glog.Warningf("Error while loading file: %v\n", err)
continue
}
dd := bytes.Split(data, []byte(defaultYamlSeparator))
for _, d := range dd {
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, gvk, err := decode([]byte(d), nil, nil)
if err != nil {
glog.Warningf("Error while decoding YAML object, err: %s\n", err)
continue
}
actualObj, err := convertToActualObject(kubeconfig, gvk, obj)
if err != nil {
glog.V(3).Infof("Failed to convert resource %s to actual k8s object: %v\n", gvk.Kind, err)
glog.V(3).Infof("Apply policy on raw resource.\n")
}
raw, err := json.Marshal(actualObj)
if err != nil {
glog.Warningf("Error while marshalling manifest, err: %v\n", err)
continue
}
gvkInfo := &metav1.GroupVersionKind{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind}
resources = append(resources, &resourceInfo{rawResource: raw, gvk: gvkInfo})
}
}
return resources, err
}
func convertToActualObject(kubeconfig string, gvk *schema.GroupVersionKind, obj runtime.Object) (interface{}, error) {
clientConfig, err := createClientConfig(kubeconfig)
if err != nil {
return obj, err
}
dynamicClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
return obj, err
}
kclient, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return obj, err
}
asUnstructured := &unstructured.Unstructured{}
if err := scheme.Scheme.Convert(obj, asUnstructured, nil); err != nil {
return obj, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(kclient.Discovery()))
mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
if err != nil {
return obj, err
}
actualObj, err := dynamicClient.Resource(mapping.Resource).Namespace("default").Create(asUnstructured, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
if err != nil {
return obj, err
}
return actualObj, nil
}

View file

@ -1,107 +0,0 @@
package apply
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
yamlv2 "gopkg.in/yaml.v2"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
rest "k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
)
func createClientConfig(kubeconfig string) (*rest.Config, error) {
if kubeconfig == "" {
defaultKC := defaultKubeconfigPath()
if _, err := os.Stat(defaultKC); err == nil {
kubeconfig = defaultKC
}
}
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
func defaultKubeconfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
glog.Warningf("Warning: failed to get home dir: %v\n", err)
return ""
}
return filepath.Join(home, ".kube", "config")
}
func loadFile(fileDir string) ([]byte, error) {
if _, err := os.Stat(fileDir); os.IsNotExist(err) {
return nil, err
}
return ioutil.ReadFile(fileDir)
}
func validateDir(args []string) (policyDir, resourceDir string, err error) {
if len(args) != 2 {
return "", "", fmt.Errorf("missing policy and/or resource manifest")
}
if strings.HasPrefix(args[0], "@") {
policyDir = args[0][1:]
}
if strings.HasPrefix(args[1], "@") {
resourceDir = args[1][1:]
}
return
}
func prettyPrint(data []byte) ([]byte, error) {
out := make(map[interface{}]interface{})
if err := yamlv2.Unmarshal(data, &out); err != nil {
return nil, err
}
return yamlv2.Marshal(&out)
}
func isDir(dir string) (bool, error) {
fi, err := os.Stat(dir)
if err != nil {
return false, err
}
return fi.IsDir(), nil
}
func scanDir(dir string) ([]string, error) {
var res []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("prevent panic by handling failure accessing a path %q: %v", dir, err)
}
res = append(res, path)
return nil
})
if err != nil {
return nil, fmt.Errorf("error walking the path %q: %v", dir, err)
}
return res[1:], nil
}
//ConvertToUnstructured converts the resource to unstructured format
func ConvertToUnstructured(data []byte) (*unstructured.Unstructured, error) {
resource := &unstructured.Unstructured{}
err := resource.UnmarshalJSON(data)
if err != nil {
glog.V(4).Infof("failed to unmarshall resource: %v", err)
return nil, err
}
return resource, nil
}

View file

@ -1,27 +0,0 @@
package cmd
import (
"io"
"os"
"github.com/nirmata/kyverno/pkg/kyverno/apply"
"github.com/nirmata/kyverno/pkg/kyverno/version"
"github.com/spf13/cobra"
)
// NewDefaultKyvernoCommand ...
func NewDefaultKyvernoCommand() *cobra.Command {
return NewKyvernoCommand(os.Stdin, os.Stdout, os.Stderr)
}
// NewKyvernoCommand returns the new kynerno command
func NewKyvernoCommand(in io.Reader, out, errout io.Writer) *cobra.Command {
cmds := &cobra.Command{
Use: "kyverno",
Short: "kyverno manages native policies of Kubernetes",
}
cmds.AddCommand(apply.NewCmdApply(in, out, errout))
cmds.AddCommand(version.NewCmdVersion(out))
return cmds
}

View file

@ -1,29 +0,0 @@
package version
import (
"fmt"
"io"
"github.com/nirmata/kyverno/pkg/version"
"github.com/spf13/cobra"
)
// NewCmdVersion is a command to display the build version
func NewCmdVersion(cmdOut io.Writer) *cobra.Command {
versionCmd := &cobra.Command{
Use: "version",
Short: "",
Run: func(cmd *cobra.Command, args []string) {
showVersion()
},
}
return versionCmd
}
func showVersion() {
fmt.Printf("Version: %s\n", version.BuildVersion)
fmt.Printf("Time: %s\n", version.BuildTime)
fmt.Printf("Git commit ID: %s\n", version.BuildHash)
}

View file

@ -1,250 +0,0 @@
package namespace
import (
"time"
"k8s.io/apimachinery/pkg/util/wait"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/config"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policystore"
"github.com/nirmata/kyverno/pkg/policyviolation"
"k8s.io/apimachinery/pkg/api/errors"
kyvernoclient "github.com/nirmata/kyverno/pkg/client/clientset/versioned"
kyvernoinformer "github.com/nirmata/kyverno/pkg/client/informers/externalversions/kyverno/v1"
kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1"
v1 "k8s.io/api/core/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
v1Informer "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
)
const (
// maxRetries is the number of times a Namespace will be processed for a policy before its dropped from the queue
maxRetries = 15
)
//NamespaceController watches the 'Namespace' resource creation/update and applied the generation rules on them
type NamespaceController struct {
client *client.Client
kyvernoClient *kyvernoclient.Clientset
syncHandler func(nsKey string) error
enqueueNs func(ns *v1.Namespace)
//nsLister provides expansion to the namespace lister to inject GVK for the resource
nsLister NamespaceListerExpansion
// nsSynced returns true if the Namespace store has been synced at least once
nsSynced cache.InformerSynced
// pvLister can list/get policy violation from the shared informer's store
pLister kyvernolister.ClusterPolicyLister
// pSynced retrns true if the Policy store has been synced at least once
pSynced cache.InformerSynced
// API to send policy stats for aggregation
policyStatus policy.PolicyStatusInterface
// eventGen provides interface to generate evenets
eventGen event.Interface
// Namespaces that need to be synced
queue workqueue.RateLimitingInterface
// Resource manager, manages the mapping for already processed resource
rm resourceManager
// helpers to validate against current loaded configuration
configHandler config.Interface
// store to hold policy meta data for faster lookup
pMetaStore policystore.LookupInterface
// policy violation generator
pvGenerator policyviolation.GeneratorInterface
}
//NewNamespaceController returns a new Controller to manage generation rules
func NewNamespaceController(kyvernoClient *kyvernoclient.Clientset,
client *client.Client,
nsInformer v1Informer.NamespaceInformer,
pInformer kyvernoinformer.ClusterPolicyInformer,
policyStatus policy.PolicyStatusInterface,
eventGen event.Interface,
configHandler config.Interface,
pvGenerator policyviolation.GeneratorInterface,
pMetaStore policystore.LookupInterface) *NamespaceController {
//TODO: do we need to event recorder for this controller?
// create the controller
nsc := &NamespaceController{
client: client,
kyvernoClient: kyvernoClient,
eventGen: eventGen,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "namespace"),
configHandler: configHandler,
pMetaStore: pMetaStore,
pvGenerator: pvGenerator,
}
nsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: nsc.addNamespace,
UpdateFunc: nsc.updateNamespace,
DeleteFunc: nsc.deleteNamespace,
})
pInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: nsc.addPolicy,
UpdateFunc: nsc.updatePolicy,
})
nsc.enqueueNs = nsc.enqueue
nsc.syncHandler = nsc.syncNamespace
nsc.nsLister = NewNamespaceLister(nsInformer.Lister())
nsc.nsSynced = nsInformer.Informer().HasSynced
nsc.pLister = pInformer.Lister()
nsc.pSynced = pInformer.Informer().HasSynced
nsc.policyStatus = policyStatus
// resource manager
// rebuild after 300 seconds/ 5 mins
nsc.rm = NewResourceManager(300)
return nsc
}
func (nsc *NamespaceController) addPolicy(obj interface{}) {
p := obj.(*kyverno.ClusterPolicy)
// check if the policy has generate rule
if generateRuleExists(p) {
// process policy
nsc.processPolicy(p)
}
}
func (nsc *NamespaceController) updatePolicy(old, cur interface{}) {
curP := cur.(*kyverno.ClusterPolicy)
// check if the policy has generate rule
if generateRuleExists(curP) {
// process policy
nsc.processPolicy(curP)
}
}
func (nsc *NamespaceController) addNamespace(obj interface{}) {
ns := obj.(*v1.Namespace)
glog.V(4).Infof("Adding Namespace %s", ns.Name)
nsc.enqueueNs(ns)
}
func (nsc *NamespaceController) updateNamespace(old, cur interface{}) {
oldNs := old.(*v1.Namespace)
curNs := cur.(*v1.Namespace)
if curNs.ResourceVersion == oldNs.ResourceVersion {
// Periodic resync will send update events for all known Namespace.
// Two different versions of the same replica set will always have different RVs.
return
}
glog.V(4).Infof("Updating Namesapce %s", curNs.Name)
//TODO: anything to be done here?
}
func (nsc *NamespaceController) deleteNamespace(obj interface{}) {
ns, _ := obj.(*v1.Namespace)
glog.V(4).Infof("Deleting Namespace %s", ns.Name)
//TODO: anything to be done here?
}
func (nsc *NamespaceController) enqueue(ns *v1.Namespace) {
key, err := cache.MetaNamespaceKeyFunc(ns)
if err != nil {
glog.Error(err)
return
}
nsc.queue.Add(key)
}
//Run to run the controller
func (nsc *NamespaceController) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer nsc.queue.ShutDown()
glog.Info("Starting namespace controller")
defer glog.Info("Shutting down namespace controller")
if ok := cache.WaitForCacheSync(stopCh, nsc.nsSynced, nsc.pSynced); !ok {
glog.Error("namespace generator: failed to sync cache")
return
}
for i := 0; i < workers; i++ {
go wait.Until(nsc.worker, time.Second, stopCh)
}
<-stopCh
}
// worker runs a worker thread that just dequeues items, processes them, and marks them done.
// It enforces that the syncHandler is never invoked concurrently with the same key.
func (nsc *NamespaceController) worker() {
for nsc.processNextWorkItem() {
}
}
func (nsc *NamespaceController) processNextWorkItem() bool {
key, quit := nsc.queue.Get()
if quit {
return false
}
defer nsc.queue.Done(key)
err := nsc.syncHandler(key.(string))
nsc.handleErr(err, key)
return true
}
func (nsc *NamespaceController) handleErr(err error, key interface{}) {
if err == nil {
nsc.queue.Forget(key)
return
}
if nsc.queue.NumRequeues(key) < maxRetries {
glog.V(2).Infof("Error syncing namespace %v: %v", key, err)
nsc.queue.AddRateLimited(key)
return
}
utilruntime.HandleError(err)
glog.V(2).Infof("Dropping namespace %q out of the queue: %v", key, err)
nsc.queue.Forget(key)
}
func (nsc *NamespaceController) syncNamespace(key string) error {
startTime := time.Now()
glog.V(4).Infof("Started syncing namespace %q (%v)", key, startTime)
defer func() {
glog.V(4).Infof("Finished syncing namespace %q (%v)", key, time.Since(startTime))
}()
namespace, err := nsc.nsLister.GetResource(key)
if errors.IsNotFound(err) {
glog.V(2).Infof("namespace %v has been deleted", key)
return nil
}
if err != nil {
return err
}
// Deep-copy otherwise we are mutating our cache.
// TODO: Deep-copy only when needed.
n := namespace.DeepCopy()
// skip processing namespace if its been filtered
// exclude the filtered resources
if nsc.configHandler.ToFilter("", namespace.Name, "") {
//TODO: improve the text
glog.V(4).Infof("excluding namespace %s as its a filtered resource", namespace.Name)
return nil
}
// process generate rules
engineResponses := nsc.processNamespace(*n)
// report errors
nsc.report(engineResponses)
return nil
}

View file

@ -1,50 +0,0 @@
package namespace
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
v1CoreLister "k8s.io/client-go/listers/core/v1"
)
//NamespaceListerExpansion ...
type NamespaceListerExpansion interface {
v1CoreLister.NamespaceLister
// List lists all Namespaces in the indexer.
ListResources(selector labels.Selector) (ret []*v1.Namespace, err error)
// GetsResource and injects gvk
GetResource(name string) (*v1.Namespace, error)
}
//NamespaceLister ...
type NamespaceLister struct {
v1CoreLister.NamespaceLister
}
//NewNamespaceLister returns a new NamespaceLister
func NewNamespaceLister(nsLister v1CoreLister.NamespaceLister) NamespaceListerExpansion {
nsl := NamespaceLister{
nsLister,
}
return &nsl
}
//ListResources is a wrapper to List and adds the resource kind information
// as the lister is specific to a gvk we can harcode the values here
func (nsl *NamespaceLister) ListResources(selector labels.Selector) (ret []*v1.Namespace, err error) {
namespaces, err := nsl.List(selector)
for index := range namespaces {
namespaces[index].SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace"))
}
return namespaces, err
}
//GetResource is a wrapper to get the resource and inject the GVK
func (nsl *NamespaceLister) GetResource(name string) (*v1.Namespace, error) {
namespace, err := nsl.Get(name)
if err != nil {
return nil, err
}
namespace.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("Namespace"))
return namespace, err
}

View file

@ -1,216 +0,0 @@
package namespace
import (
"sync"
"time"
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/response"
policyctr "github.com/nirmata/kyverno/pkg/policy"
"github.com/nirmata/kyverno/pkg/policystore"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type resourceManager interface {
ProcessResource(policy, pv, kind, ns, name, rv string) bool
//TODO removeResource(kind, ns, name string) error
RegisterResource(policy, pv, kind, ns, name, rv string)
// reload
Drop()
}
// ResourceManager stores the details on already processed resources for caching
type ResourceManager struct {
// we drop and re-build the cache
// based on the memory consumer of by the map
data map[string]interface{}
mux sync.RWMutex
time time.Time
rebuildTime int64 // after how many seconds should we rebuild the cache
}
//NewResourceManager returns a new ResourceManager
func NewResourceManager(rebuildTime int64) *ResourceManager {
rm := ResourceManager{
data: make(map[string]interface{}),
time: time.Now(),
rebuildTime: rebuildTime,
}
// set time it was built
return &rm
}
var empty struct{}
//RegisterResource stores if the policy is processed on this resource version
func (rm *ResourceManager) RegisterResource(policy, pv, kind, ns, name, rv string) {
rm.mux.Lock()
defer rm.mux.Unlock()
// add the resource
key := buildKey(policy, pv, kind, ns, name, rv)
rm.data[key] = empty
}
//ProcessResource returns true if the policy was not applied on the resource
func (rm *ResourceManager) ProcessResource(policy, pv, kind, ns, name, rv string) bool {
rm.mux.RLock()
defer rm.mux.RUnlock()
key := buildKey(policy, pv, kind, ns, name, rv)
_, ok := rm.data[key]
return !ok
}
//Drop drop the cache after every rebuild interval mins
//TODO: or drop based on the size
func (rm *ResourceManager) Drop() {
timeSince := time.Since(rm.time)
glog.V(4).Infof("time since last cache reset time %v is %v", rm.time, timeSince)
glog.V(4).Infof("cache rebuild time %v", time.Duration(rm.rebuildTime)*time.Second)
if timeSince > time.Duration(rm.rebuildTime)*time.Second {
rm.mux.Lock()
defer rm.mux.Unlock()
rm.data = map[string]interface{}{}
rm.time = time.Now()
glog.V(4).Infof("dropping cache at time %v", rm.time)
}
}
func buildKey(policy, pv, kind, ns, name, rv string) string {
return policy + "/" + pv + "/" + kind + "/" + ns + "/" + name + "/" + rv
}
func (nsc *NamespaceController) processNamespace(namespace corev1.Namespace) []response.EngineResponse {
// convert to unstructured
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&namespace)
if err != nil {
glog.Infof("unable to convert to unstructured, not processing any policies: %v", err)
return nil
}
nsc.rm.Drop()
ns := unstructured.Unstructured{Object: unstr}
// get all the policies that have a generate rule and resource description satisfies the namespace
// apply policy on resource
policies := listpolicies(ns, nsc.pMetaStore)
var engineResponses []response.EngineResponse
for _, policy := range policies {
// pre-processing, check if the policy and resource version has been processed before
if !nsc.rm.ProcessResource(policy.Name, policy.ResourceVersion, ns.GetKind(), ns.GetNamespace(), ns.GetName(), ns.GetResourceVersion()) {
glog.V(4).Infof("policy %s with resource version %s already processed on resource %s/%s/%s with resource version %s", policy.Name, policy.ResourceVersion, ns.GetKind(), ns.GetNamespace(), ns.GetName(), ns.GetResourceVersion())
continue
}
engineResponse := applyPolicy(nsc.client, ns, policy, nsc.policyStatus)
engineResponses = append(engineResponses, engineResponse)
// post-processing, register the resource as processed
nsc.rm.RegisterResource(policy.GetName(), policy.GetResourceVersion(), ns.GetKind(), ns.GetNamespace(), ns.GetName(), ns.GetResourceVersion())
}
return engineResponses
}
func generateRuleExists(policy *kyverno.ClusterPolicy) bool {
for _, rule := range policy.Spec.Rules {
if rule.Generation != (kyverno.Generation{}) {
return true
}
}
return false
}
func (nsc *NamespaceController) processPolicy(policy *kyverno.ClusterPolicy) {
filteredNamespaces := []*corev1.Namespace{}
// get namespaces that policy applies on
namespaces, err := nsc.nsLister.ListResources(labels.NewSelector())
if err != nil {
glog.Errorf("failed to get list namespaces: %v", err)
return
}
for _, namespace := range namespaces {
// convert to unstructured
unstr, err := runtime.DefaultUnstructuredConverter.ToUnstructured(namespace)
if err != nil {
glog.Infof("unable to convert to unstructured, not processing any policies: %v", err)
continue
}
ns := unstructured.Unstructured{Object: unstr}
for _, rule := range policy.Spec.Rules {
if rule.Generation == (kyverno.Generation{}) {
continue
}
ok := engine.MatchesResourceDescription(ns, rule)
if !ok {
glog.V(4).Infof("namespace %s does not satisfy the resource description for the policy %s rule %s", ns.GetName(), policy.Name, rule.Name)
continue
}
glog.V(4).Infof("namespace %s satisfies resource description for policy %s rule %s", ns.GetName(), policy.Name, rule.Name)
filteredNamespaces = append(filteredNamespaces, namespace)
}
}
// list of namespaces that the policy applies on
for _, ns := range filteredNamespaces {
glog.V(4).Infof("policy %s with generate rule: namespace %s to be processed ", policy.Name, ns.Name)
nsc.addNamespace(ns)
}
}
func listpolicies(ns unstructured.Unstructured, pMetaStore policystore.LookupInterface) []kyverno.ClusterPolicy {
var filteredpolicies []kyverno.ClusterPolicy
glog.V(4).Infof("listing policies for namespace %s", ns.GetName())
policies, err := pMetaStore.ListAll()
if err != nil {
glog.Errorf("failed to get list policies: %v", err)
return nil
}
for _, policy := range policies {
for _, rule := range policy.Spec.Rules {
if rule.Generation == (kyverno.Generation{}) {
continue
}
ok := engine.MatchesResourceDescription(ns, rule)
if !ok {
glog.V(4).Infof("namespace %s does not satisfy the resource description for the policy %s rule %s", ns.GetName(), policy.Name, rule.Name)
continue
}
glog.V(4).Infof("namespace %s satisfies resource description for policy %s rule %s", ns.GetName(), policy.Name, rule.Name)
filteredpolicies = append(filteredpolicies, policy)
}
}
return filteredpolicies
}
func applyPolicy(client *client.Client, resource unstructured.Unstructured, p kyverno.ClusterPolicy, policyStatus policyctr.PolicyStatusInterface) response.EngineResponse {
startTime := time.Now()
glog.V(4).Infof("Started apply policy %s on resource %s/%s/%s (%v)", p.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), startTime)
defer func() {
glog.V(4).Infof("Finished applying %s on resource %s/%s/%s (%v)", p.Name, resource.GetKind(), resource.GetNamespace(), resource.GetName(), time.Since(startTime))
}()
// build context
ctx := context.NewContext()
ctx.AddResource(transformResource(resource))
policyContext := engine.PolicyContext{
NewResource: resource,
Policy: p,
Client: client,
Context: ctx,
}
engineResponse := engine.Generate(policyContext)
return engineResponse
}
func transformResource(resource unstructured.Unstructured) []byte {
data, err := resource.MarshalJSON()
if err != nil {
glog.Errorf("failed to marshall resource %v: %v", resource, err)
return nil
}
return data
}

View file

@ -1,63 +0,0 @@
package namespace
import (
"fmt"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/response"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policyviolation"
)
func (nsc *NamespaceController) report(engineResponses []response.EngineResponse) {
// generate events
eventInfos := generateEvents(engineResponses)
nsc.eventGen.Add(eventInfos...)
// generate policy violations
pvInfos := policyviolation.GeneratePVsFromEngineResponse(engineResponses)
nsc.pvGenerator.Add(pvInfos...)
}
func generateEvents(ers []response.EngineResponse) []event.Info {
var eventInfos []event.Info
for _, er := range ers {
if er.IsSuccesful() {
continue
}
eventInfos = append(eventInfos, generateEventsPerEr(er)...)
}
return eventInfos
}
func generateEventsPerEr(er response.EngineResponse) []event.Info {
var eventInfos []event.Info
glog.V(4).Infof("reporting results for policy '%s' application on resource '%s/%s/%s'", er.PolicyResponse.Policy, er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name)
for _, rule := range er.PolicyResponse.Rules {
if rule.Success {
continue
}
// generate event on resource for each failed rule
glog.V(4).Infof("generation event on resource '%s/%s' for policy '%s'", er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Name, er.PolicyResponse.Policy)
e := event.Info{}
e.Kind = er.PolicyResponse.Resource.Kind
e.Namespace = "" // event generate on namespace resource
e.Name = er.PolicyResponse.Resource.Name
e.Reason = "Failure"
e.Source = event.GeneratePolicyController
e.Message = fmt.Sprintf("policy '%s' (%s) rule '%s' not satisfied. %v", er.PolicyResponse.Policy, rule.Type, rule.Name, rule.Message)
eventInfos = append(eventInfos, e)
}
if er.IsSuccesful() {
return eventInfos
}
// generate a event on policy for all failed rules
glog.V(4).Infof("generation event on policy '%s'", er.PolicyResponse.Policy)
e := event.Info{}
e.Kind = "ClusterPolicy"
e.Namespace = ""
e.Name = er.PolicyResponse.Policy
e.Reason = "Failure"
e.Source = event.GeneratePolicyController
e.Message = fmt.Sprintf("policy '%s' rules '%v' on resource '%s/%s/%s' not stasified", er.PolicyResponse.Policy, er.GetFailedRules(), er.PolicyResponse.Resource.Kind, er.PolicyResponse.Resource.Namespace, er.PolicyResponse.Resource.Name)
return eventInfos
}

73
pkg/policy/background.go Normal file
View file

@ -0,0 +1,73 @@
package policy
import (
"fmt"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
"github.com/nirmata/kyverno/pkg/engine/context"
"github.com/nirmata/kyverno/pkg/engine/variables"
)
//ContainsUserInfo returns error is userInfo is defined
func ContainsUserInfo(policy kyverno.ClusterPolicy) error {
var err error
// iterate of the policy rules to identify if userInfo is used
for idx, rule := range policy.Spec.Rules {
if path := userInfoDefined(rule.MatchResources.UserInfo); path != "" {
return fmt.Errorf("userInfo variable used at path: spec/rules[%d]/match/%s", idx, path)
}
if path := userInfoDefined(rule.ExcludeResources.UserInfo); path != "" {
return fmt.Errorf("userInfo variable used at path: spec/rules[%d]/exclude/%s", idx, path)
}
// variable defined with user information
// - condition.key
// - condition.value
// - mutate.overlay
// - validate.pattern
// - validate.anyPattern[*]
// variables to filter
// - request.userInfo*
// - serviceAccountName
// - serviceAccountNamespace
filterVars := []string{"request.userInfo*", "serviceAccountName", "serviceAccountNamespace"}
ctx := context.NewContext(filterVars...)
for condIdx, condition := range rule.Conditions {
if condition.Key, err = variables.SubstituteVars(ctx, condition.Key); err != nil {
return fmt.Errorf("userInfo variable used at spec/rules[%d]/condition[%d]/key", idx, condIdx)
}
if condition.Value, err = variables.SubstituteVars(ctx, condition.Value); err != nil {
return fmt.Errorf("userInfo variable used at spec/rules[%d]/condition[%d]/value", idx, condIdx)
}
}
if rule.Mutation.Overlay, err = variables.SubstituteVars(ctx, rule.Mutation.Overlay); err != nil {
return fmt.Errorf("userInfo variable used at spec/rules[%d]/mutate/overlay", idx)
}
if rule.Validation.Pattern, err = variables.SubstituteVars(ctx, rule.Validation.Pattern); err != nil {
return fmt.Errorf("userInfo variable used at spec/rules[%d]/validate/pattern", idx)
}
for idx2, pattern := range rule.Validation.AnyPattern {
if rule.Validation.AnyPattern[idx2], err = variables.SubstituteVars(ctx, pattern); err != nil {
return fmt.Errorf("userInfo variable used at spec/rules[%d]/validate/anyPattern[%d]", idx, idx2)
}
}
}
return nil
}
func userInfoDefined(ui kyverno.UserInfo) string {
if len(ui.Roles) > 0 {
return "roles"
}
if len(ui.ClusterRoles) > 0 {
return "clusterRoles"
}
if len(ui.Subjects) > 0 {
return "subjects"
}
return ""
}

View file

@ -12,7 +12,6 @@ import (
kyvernolister "github.com/nirmata/kyverno/pkg/client/listers/kyverno/v1"
"github.com/nirmata/kyverno/pkg/config"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine/policy"
"github.com/nirmata/kyverno/pkg/event"
"github.com/nirmata/kyverno/pkg/policystore"
"github.com/nirmata/kyverno/pkg/policyviolation"
@ -155,7 +154,7 @@ func (pc *PolicyController) addPolicy(obj interface{}) {
// TODO: code might seem vague, awaiting resolution of issue https://github.com/nirmata/kyverno/issues/598
if p.Spec.Background == nil {
// if userInfo is not defined in policy we process the policy
if err := policy.ContainsUserInfo(*p); err != nil {
if err := ContainsUserInfo(*p); err != nil {
return
}
} else {
@ -164,7 +163,7 @@ func (pc *PolicyController) addPolicy(obj interface{}) {
}
// If userInfo is used then skip the policy
// ideally this should be handled by background flag only
if err := policy.ContainsUserInfo(*p); err != nil {
if err := ContainsUserInfo(*p); err != nil {
// contains userInfo used in policy
return
}
@ -190,7 +189,7 @@ func (pc *PolicyController) updatePolicy(old, cur interface{}) {
// TODO: code might seem vague, awaiting resolution of issue https://github.com/nirmata/kyverno/issues/598
if curP.Spec.Background == nil {
// if userInfo is not defined in policy we process the policy
if err := policy.ContainsUserInfo(*curP); err != nil {
if err := ContainsUserInfo(*curP); err != nil {
return
}
} else {
@ -199,7 +198,7 @@ func (pc *PolicyController) updatePolicy(old, cur interface{}) {
}
// If userInfo is used then skip the policy
// ideally this should be handled by background flag only
if err := policy.ContainsUserInfo(*curP); err != nil {
if err := ContainsUserInfo(*curP); err != nil {
// contains userInfo used in policy
return
}

View file

@ -30,7 +30,7 @@ func Validate(p kyverno.ClusterPolicy) error {
// policy.spec.background -> "true"
// - cannot use variables with request.userInfo
// - cannot define userInfo(roles, cluserRoles, subjects) for filtering (match & exclude)
return fmt.Errorf("userInfo not allowed in background policy mode. Failure path %s", err)
return fmt.Errorf("userInfo is not allowed in match or exclude when backgroud policy mode is true. Set spec.background=false to disable background mode for this policy rule. %s ", err)
}
}
@ -70,6 +70,7 @@ func Validate(p kyverno.ClusterPolicy) error {
}
}
}
return nil
}

View file

@ -1190,10 +1190,7 @@ func Test_BackGroundUserInfo_match_roles(t *testing.T) {
assert.NilError(t, err)
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/roles" {
t.Error("Incorrect Path")
}
assert.Equal(t, err.Error(), "userInfo variable used at path: spec/rules[0]/match/roles")
}
func Test_BackGroundUserInfo_match_clusterRoles(t *testing.T) {
@ -1226,10 +1223,7 @@ func Test_BackGroundUserInfo_match_clusterRoles(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/clusterRoles" {
t.Log(err)
t.Error("Incorrect Path")
}
assert.Equal(t, err.Error(), "userInfo variable used at path: spec/rules[0]/match/clusterRoles")
}
func Test_BackGroundUserInfo_match_subjects(t *testing.T) {
@ -1265,10 +1259,7 @@ func Test_BackGroundUserInfo_match_subjects(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/match/subjects" {
t.Log(err)
t.Error("Incorrect Path")
}
assert.Equal(t, err.Error(), "userInfo variable used at path: spec/rules[0]/match/subjects")
}
func Test_BackGroundUserInfo_mutate_overlay1(t *testing.T) {
@ -1300,7 +1291,7 @@ func Test_BackGroundUserInfo_mutate_overlay1(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/mutate/overlay/var1/{{request.userInfo}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/mutate/overlay" {
t.Log(err)
t.Error("Incorrect Path")
}
@ -1335,7 +1326,7 @@ func Test_BackGroundUserInfo_mutate_overlay2(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/mutate/overlay/var1/{{request.userInfo.userName}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/mutate/overlay" {
t.Log(err)
t.Error("Incorrect Path")
}
@ -1370,7 +1361,7 @@ func Test_BackGroundUserInfo_validate_pattern(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/pattern/var1/{{request.userInfo}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/validate/pattern" {
t.Log(err)
t.Error("Incorrect Path")
}
@ -1409,7 +1400,7 @@ func Test_BackGroundUserInfo_validate_anyPattern(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{request.userInfo}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/validate/anyPattern[1]" {
t.Log(err)
t.Error("Incorrect Path")
}
@ -1448,7 +1439,7 @@ func Test_BackGroundUserInfo_validate_anyPattern_multiple_var(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{request.userInfo}}-{{temp}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/validate/anyPattern[1]" {
t.Log(err)
t.Error("Incorrect Path")
}
@ -1487,7 +1478,7 @@ func Test_BackGroundUserInfo_validate_anyPattern_serviceAccount(t *testing.T) {
err = ContainsUserInfo(*policy)
if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{serviceAccountName}}" {
if err.Error() != "userInfo variable used at spec/rules[0]/validate/anyPattern[1]" {
t.Log(err)
t.Error("Incorrect Path")
}

View file

@ -16,8 +16,8 @@ func GeneratePVsFromEngineResponse(ers []response.EngineResponse) (pvInfos []Inf
glog.V(4).Infof("resource %v, has not been assigned a name, not creating a policy violation for it", er.PolicyResponse.Resource)
continue
}
// skip when response succeed AND referenced paths exist
if er.IsSuccesful() && !er.IsPathNotPresent() {
// skip when response succeed
if er.IsSuccesful() {
continue
}
glog.V(4).Infof("Building policy violation for engine response %v", er)
@ -82,7 +82,7 @@ func buildPVInfo(er response.EngineResponse) Info {
func buildViolatedRules(er response.EngineResponse) []kyverno.ViolatedRule {
var violatedRules []kyverno.ViolatedRule
for _, rule := range er.PolicyResponse.Rules {
if rule.Success && !rule.PathNotPresent {
if rule.Success {
continue
}
vrule := kyverno.ViolatedRule{

View file

@ -19,17 +19,15 @@ func Test_GeneratePVsFromEngineResponse_PathNotExist(t *testing.T) {
},
Rules: []response.RuleResponse{
{
Name: "test-path-not-exist",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
Name: "test-path-not-exist",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: false,
},
{
Name: "test-path-exist",
Type: "Mutation",
Success: true,
PathNotPresent: false,
Name: "test-path-exist",
Type: "Mutation",
Success: true,
},
},
},
@ -44,11 +42,10 @@ func Test_GeneratePVsFromEngineResponse_PathNotExist(t *testing.T) {
},
Rules: []response.RuleResponse{
{
Name: "test-path-not-exist-across-policy",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
PathNotPresent: true,
Name: "test-path-not-exist-across-policy",
Type: "Mutation",
Message: "referenced paths are not present: request.object.metadata.name1",
Success: true,
},
},
},
@ -56,5 +53,5 @@ func Test_GeneratePVsFromEngineResponse_PathNotExist(t *testing.T) {
}
pvInfos := GeneratePVsFromEngineResponse(ers)
assert.Assert(t, len(pvInfos) == 2)
assert.Assert(t, len(pvInfos) == 1)
}

View file

@ -5,7 +5,6 @@ import (
"strings"
"github.com/golang/glog"
"github.com/nirmata/kyverno/pkg/engine/rbac"
v1beta1 "k8s.io/api/admission/v1beta1"
authenticationv1 "k8s.io/api/authentication/v1"
rbacv1 "k8s.io/api/rbac/v1"
@ -16,6 +15,7 @@ import (
const (
clusterrolekind = "ClusterRole"
rolekind = "Role"
SaPrefix = "system:serviceaccount:"
)
//GetRoleRef gets the list of roles and cluster roles for the incoming api-request
@ -88,27 +88,20 @@ func getRoleRefByClusterRoleBindings(clusterroleBindings []*rbacv1.ClusterRoleBi
// subject.kind can only be ServiceAccount, User and Group
func matchSubjectsMap(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
// ServiceAccount
if isServiceaccountUserInfo(userInfo.Username) {
if strings.Contains(userInfo.Username, SaPrefix) {
return matchServiceAccount(subject, userInfo)
} else {
// User or Group
return matchUserOrGroup(subject, userInfo)
}
// User or Group
return matchUserOrGroup(subject, userInfo)
}
func isServiceaccountUserInfo(username string) bool {
if strings.Contains(username, rbac.SaPrefix) {
return true
}
return false
}
// matchServiceAccount checks if userInfo sa matche the subject sa
// serviceaccount represents as saPrefix:namespace:name in userInfo
func matchServiceAccount(subject rbacv1.Subject, userInfo authenticationv1.UserInfo) bool {
subjectServiceAccount := subject.Namespace + ":" + subject.Name
if userInfo.Username[len(rbac.SaPrefix):] != subjectServiceAccount {
glog.V(3).Infof("service account not match, expect %s, got %s", subjectServiceAccount, userInfo.Username[len(rbac.SaPrefix):])
if userInfo.Username[len(SaPrefix):] != subjectServiceAccount {
glog.V(3).Infof("service account not match, expect %s, got %s", subjectServiceAccount, userInfo.Username[len(SaPrefix):])
return false
}

View file

@ -11,27 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func Test_isServiceaccountUserInfo(t *testing.T) {
tests := []struct {
username string
expected bool
}{
{
username: "system:serviceaccount:default:saconfig",
expected: true,
},
{
username: "serviceaccount:default:saconfig",
expected: false,
},
}
for _, test := range tests {
res := isServiceaccountUserInfo(test.username)
assert.Assert(t, test.expected == res)
}
}
func Test_matchServiceAccount_subject_variants(t *testing.T) {
userInfo := authenticationv1.UserInfo{
Username: "system:serviceaccount:default:saconfig",

View file

@ -114,7 +114,7 @@ func processResourceWithPatches(patch []byte, resource []byte) []byte {
func containRBACinfo(policies []kyverno.ClusterPolicy) bool {
for _, policy := range policies {
for _, rule := range policy.Spec.Rules {
if len(rule.MatchResources.Roles) > 0 || len(rule.MatchResources.ClusterRoles) > 0 {
if len(rule.MatchResources.Roles) > 0 || len(rule.MatchResources.ClusterRoles) > 0 || len(rule.ExcludeResources.Roles) > 0 || len(rule.ExcludeResources.ClusterRoles) > 0 {
return true
}
}

View file

@ -31,7 +31,7 @@ func (ws *WebhookServer) handlePolicyMutation(request *v1beta1.AdmissionRequest)
}
}
// Generate JSON Patches for defaults
patches, updateMsgs := generateJSONPatchesForDefaults(policy, request.Operation)
patches, updateMsgs := generateJSONPatchesForDefaults(policy)
if patches != nil {
patchType := v1beta1.PatchTypeJSONPatch
glog.V(4).Infof("defaulted values %v policy %s", updateMsgs, policy.Name)
@ -50,7 +50,7 @@ func (ws *WebhookServer) handlePolicyMutation(request *v1beta1.AdmissionRequest)
}
}
func generateJSONPatchesForDefaults(policy *kyverno.ClusterPolicy, operation v1beta1.Operation) ([]byte, []string) {
func generateJSONPatchesForDefaults(policy *kyverno.ClusterPolicy) ([]byte, []string) {
var patches [][]byte
var updateMsgs []string
@ -66,20 +66,18 @@ func generateJSONPatchesForDefaults(policy *kyverno.ClusterPolicy, operation v1b
updateMsgs = append(updateMsgs, updateMsg)
}
// TODO(shuting): enable this feature on policy UPDATE
if operation == v1beta1.Create {
patch, errs := generatePodControllerRule(*policy)
if len(errs) > 0 {
var errMsgs []string
for _, err := range errs {
errMsgs = append(errMsgs, err.Error())
}
glog.Errorf("failed auto generatig rule for pod controllers: %s", errMsgs)
updateMsgs = append(updateMsgs, strings.Join(errMsgs, ";"))
patch, errs := generatePodControllerRule(*policy)
if len(errs) > 0 {
var errMsgs []string
for _, err := range errs {
errMsgs = append(errMsgs, err.Error())
}
patches = append(patches, patch...)
glog.Errorf("failed auto generating rule for pod controllers: %s", errMsgs)
updateMsgs = append(updateMsgs, strings.Join(errMsgs, ";"))
}
patches = append(patches, patch...)
return utils.JoinPatches(patches), updateMsgs
}
@ -170,25 +168,73 @@ func generatePodControllerRule(policy kyverno.ClusterPolicy) (patches [][]byte,
return
}
func createRuleMap(rules []kyverno.Rule) map[string]kyvernoRule {
var ruleMap = make(map[string]kyvernoRule)
for _, rule := range rules {
var jsonFriendlyStruct kyvernoRule
jsonFriendlyStruct.Name = rule.Name
if !reflect.DeepEqual(rule.MatchResources, kyverno.MatchResources{}) {
jsonFriendlyStruct.MatchResources = rule.MatchResources.DeepCopy()
}
if !reflect.DeepEqual(rule.ExcludeResources, kyverno.ExcludeResources{}) {
jsonFriendlyStruct.ExcludeResources = rule.ExcludeResources.DeepCopy()
}
if !reflect.DeepEqual(rule.Mutation, kyverno.Mutation{}) {
jsonFriendlyStruct.Mutation = rule.Mutation.DeepCopy()
}
if !reflect.DeepEqual(rule.Validation, kyverno.Validation{}) {
jsonFriendlyStruct.Validation = rule.Validation.DeepCopy()
}
ruleMap[rule.Name] = jsonFriendlyStruct
}
return ruleMap
}
// generateRulePatches generates rule for podControllers based on scenario A and C
func generateRulePatches(policy kyverno.ClusterPolicy, controllers string) (rulePatches [][]byte, errs []error) {
var genRule kyvernoRule
insertIdx := len(policy.Spec.Rules)
ruleMap := createRuleMap(policy.Spec.Rules)
var ruleIndex = make(map[string]int)
for index, rule := range policy.Spec.Rules {
ruleIndex[rule.Name] = index
}
for _, rule := range policy.Spec.Rules {
patchPostion := insertIdx
genRule = generateRuleForControllers(rule, controllers)
if reflect.DeepEqual(genRule, kyvernoRule{}) {
continue
}
operation := "add"
if existingAutoGenRule, alreadyExists := ruleMap[genRule.Name]; alreadyExists {
existingAutoGenRuleRaw, _ := json.Marshal(existingAutoGenRule)
genRuleRaw, _ := json.Marshal(genRule)
if string(existingAutoGenRuleRaw) == string(genRuleRaw) {
continue
}
operation = "replace"
patchPostion = ruleIndex[genRule.Name]
}
// generate patch bytes
jsonPatch := struct {
Path string `json:"path"`
Op string `json:"op"`
Value interface{} `json:"value"`
}{
fmt.Sprintf("/spec/rules/%s", strconv.Itoa(insertIdx)),
"add",
fmt.Sprintf("/spec/rules/%s", strconv.Itoa(patchPostion)),
operation,
genRule,
}
pbytes, err := json.Marshal(jsonPatch)
@ -227,6 +273,10 @@ type kyvernoRule struct {
}
func generateRuleForControllers(rule kyverno.Rule, controllers string) kyvernoRule {
if strings.HasPrefix(rule.Name, "autogen-") {
return kyvernoRule{}
}
match := rule.MatchResources
exclude := rule.ExcludeResources
if !utils.ContainsString(match.ResourceDescription.Kinds, "Pod") ||

View file

@ -6,7 +6,7 @@ import (
"github.com/golang/glog"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
policyvalidate "github.com/nirmata/kyverno/pkg/engine/policy"
policyvalidate "github.com/nirmata/kyverno/pkg/policy"
v1beta1 "k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -35,7 +35,6 @@ func (ws *WebhookServer) handlePolicyValidation(request *v1beta1.AdmissionReques
},
}
}
if admissionResp.Allowed {
// if the policy contains mutating & validation rules and it config does not exist we create one
// queue the request

View file

@ -6,7 +6,11 @@ metadata:
app: myapp
spec:
containers:
- name: nginx
image: nginx
- name: goproxy
image: k8s.gcr.io/goproxy:0.1
ports:
- containerPort: 8080
readinessProbe:
periodSeconds: 5
tcpSocket:
port: 8080
periodSeconds: 10