mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-28 02:18:15 +00:00
add support for Kubernetes API server POST (#6948)
* allow POST for Kubernetes API calls Signed-off-by: Jim Bugwadia <jim@nirmata.com> * add kuttl tests Signed-off-by: Jim Bugwadia <jim@nirmata.com> * fmt and undo local changes Signed-off-by: Jim Bugwadia <jim@nirmata.com> * fix codegen and unit test Signed-off-by: Jim Bugwadia <jim@nirmata.com> * fix unit test Signed-off-by: Jim Bugwadia <jim@nirmata.com> * fix tests and extends docs Signed-off-by: Jim Bugwadia <jim@nirmata.com> --------- Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
parent
deefe8eef3
commit
0c22858bbc
19 changed files with 3857 additions and 3705 deletions
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/sigstore/k8s-manifest-sigstore/pkg/k8smanifest"
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/pod-security-admission/api"
|
||||
|
@ -116,12 +115,22 @@ type ConfigMapReference struct {
|
|||
}
|
||||
|
||||
type APICall struct {
|
||||
// URLPath is the URL path to be used in the HTTP GET request to the
|
||||
// URLPath is the URL path to be used in the HTTP GET or POST request to the
|
||||
// Kubernetes API server (e.g. "/api/v1/namespaces" or "/apis/apps/v1/deployments").
|
||||
// The format required is the same format used by the `kubectl get --raw` command.
|
||||
// See https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls
|
||||
// for details.
|
||||
// +kubebuilder:validation:Optional
|
||||
URLPath string `json:"urlPath" yaml:"urlPath"`
|
||||
|
||||
// Method is the HTTP request type (GET or POST).
|
||||
// +kubebuilder:default=GET
|
||||
Method Method `json:"method,omitempty" yaml:"method,omitempty"`
|
||||
|
||||
// Data specifies the POST data sent to the server.
|
||||
// +kubebuilder:validation:Optional
|
||||
Data []RequestData `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
|
||||
// Service is an API call to a JSON web service
|
||||
// +kubebuilder:validation:Optional
|
||||
Service *ServiceCall `json:"service,omitempty" yaml:"service,omitempty"`
|
||||
|
@ -136,22 +145,14 @@ type APICall struct {
|
|||
}
|
||||
|
||||
type ServiceCall struct {
|
||||
// URL is the JSON web service URL.
|
||||
// The typical format is `https://{service}.{namespace}:{port}/{path}`.
|
||||
URL string `json:"urlPath" yaml:"urlPath"`
|
||||
// URL is the JSON web service URL. A typical form is
|
||||
// `https://{service}.{namespace}:{port}/{path}`.
|
||||
URL string `json:"url" yaml:"url"`
|
||||
|
||||
// CABundle is a PEM encoded CA bundle which will be used to validate
|
||||
// the server certificate.
|
||||
// +kubebuilder:validation:Optional
|
||||
CABundle string `json:"caBundle" yaml:"caBundle"`
|
||||
|
||||
// Method is the HTTP request type (GET or POST).
|
||||
// +kubebuilder:default=GET
|
||||
Method Method `json:"requestType" yaml:"requestType"`
|
||||
|
||||
// Data specifies the POST data sent to the server.
|
||||
// +kubebuilder:validation:Optional
|
||||
Data []RequestData `json:"data" yaml:"data"`
|
||||
}
|
||||
|
||||
// Method is a HTTP request type.
|
||||
|
@ -164,7 +165,7 @@ type RequestData struct {
|
|||
Key string `json:"key" yaml:"key"`
|
||||
|
||||
// Value is the data value
|
||||
Value *apiextensionsv1.JSON `json:"value" yaml:"value"`
|
||||
Value *apiextv1.JSON `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// Condition defines variable-based conditional criteria for rule execution.
|
||||
|
|
|
@ -32,10 +32,17 @@ import (
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *APICall) DeepCopyInto(out *APICall) {
|
||||
*out = *in
|
||||
if in.Data != nil {
|
||||
in, out := &in.Data, &out.Data
|
||||
*out = make([]RequestData, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Service != nil {
|
||||
in, out := &in.Service, &out.Service
|
||||
*out = new(ServiceCall)
|
||||
(*in).DeepCopyInto(*out)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1166,13 +1173,6 @@ func (in *SecretReference) DeepCopy() *SecretReference {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceCall) DeepCopyInto(out *ServiceCall) {
|
||||
*out = *in
|
||||
if in.Data != nil {
|
||||
in, out := &in.Data, &out.Data
|
||||
*out = make([]RequestData, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceCall.
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -143,6 +143,23 @@ spec:
|
|||
server, or other JSON web service. The data returned is stored
|
||||
in the context with the name for the context entry.
|
||||
properties:
|
||||
data:
|
||||
description: Data specifies the POST data sent to the server.
|
||||
items:
|
||||
description: RequestData contains the HTTP POST data
|
||||
properties:
|
||||
key:
|
||||
description: Key is a unique identifier for the data
|
||||
value
|
||||
type: string
|
||||
value:
|
||||
description: Value is the data value
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
jmesPath:
|
||||
description: JMESPath is an optional JSON Match Expression
|
||||
that can be used to transform the JSON response returned
|
||||
|
@ -151,6 +168,13 @@ spec:
|
|||
will return the total count of deployments across all
|
||||
namespaces.
|
||||
type: string
|
||||
method:
|
||||
default: GET
|
||||
description: Method is the HTTP request type (GET or POST).
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
type: string
|
||||
service:
|
||||
description: Service is an API call to a JSON web service
|
||||
properties:
|
||||
|
@ -158,45 +182,20 @@ spec:
|
|||
description: CABundle is a PEM encoded CA bundle which
|
||||
will be used to validate the server certificate.
|
||||
type: string
|
||||
data:
|
||||
description: Data specifies the POST data sent to the
|
||||
server.
|
||||
items:
|
||||
description: RequestData contains the HTTP POST data
|
||||
properties:
|
||||
key:
|
||||
description: Key is a unique identifier for the
|
||||
data value
|
||||
type: string
|
||||
value:
|
||||
description: Value is the data value
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
requestType:
|
||||
default: GET
|
||||
description: Method is the HTTP request type (GET or
|
||||
POST).
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
type: string
|
||||
urlPath:
|
||||
description: URL is the JSON web service URL. The typical
|
||||
format is `https://{service}.{namespace}:{port}/{path}`.
|
||||
url:
|
||||
description: URL is the JSON web service URL. A typical
|
||||
form is `https://{service}.{namespace}:{port}/{path}`.
|
||||
type: string
|
||||
required:
|
||||
- requestType
|
||||
- urlPath
|
||||
- url
|
||||
type: object
|
||||
urlPath:
|
||||
description: URLPath is the URL path to be used in the HTTP
|
||||
GET request to the Kubernetes API server (e.g. "/api/v1/namespaces"
|
||||
or "/apis/apps/v1/deployments"). The format required
|
||||
is the same format used by the `kubectl get --raw` command.
|
||||
GET or POST request to the Kubernetes API server (e.g.
|
||||
"/api/v1/namespaces" or "/apis/apps/v1/deployments").
|
||||
The format required is the same format used by the `kubectl
|
||||
get --raw` command. See https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls
|
||||
for details.
|
||||
type: string
|
||||
type: object
|
||||
configMap:
|
||||
|
|
|
@ -143,6 +143,23 @@ spec:
|
|||
server, or other JSON web service. The data returned is stored
|
||||
in the context with the name for the context entry.
|
||||
properties:
|
||||
data:
|
||||
description: Data specifies the POST data sent to the server.
|
||||
items:
|
||||
description: RequestData contains the HTTP POST data
|
||||
properties:
|
||||
key:
|
||||
description: Key is a unique identifier for the data
|
||||
value
|
||||
type: string
|
||||
value:
|
||||
description: Value is the data value
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
jmesPath:
|
||||
description: JMESPath is an optional JSON Match Expression
|
||||
that can be used to transform the JSON response returned
|
||||
|
@ -151,6 +168,13 @@ spec:
|
|||
will return the total count of deployments across all
|
||||
namespaces.
|
||||
type: string
|
||||
method:
|
||||
default: GET
|
||||
description: Method is the HTTP request type (GET or POST).
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
type: string
|
||||
service:
|
||||
description: Service is an API call to a JSON web service
|
||||
properties:
|
||||
|
@ -158,45 +182,20 @@ spec:
|
|||
description: CABundle is a PEM encoded CA bundle which
|
||||
will be used to validate the server certificate.
|
||||
type: string
|
||||
data:
|
||||
description: Data specifies the POST data sent to the
|
||||
server.
|
||||
items:
|
||||
description: RequestData contains the HTTP POST data
|
||||
properties:
|
||||
key:
|
||||
description: Key is a unique identifier for the
|
||||
data value
|
||||
type: string
|
||||
value:
|
||||
description: Value is the data value
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
requestType:
|
||||
default: GET
|
||||
description: Method is the HTTP request type (GET or
|
||||
POST).
|
||||
enum:
|
||||
- GET
|
||||
- POST
|
||||
type: string
|
||||
urlPath:
|
||||
description: URL is the JSON web service URL. The typical
|
||||
format is `https://{service}.{namespace}:{port}/{path}`.
|
||||
url:
|
||||
description: URL is the JSON web service URL. A typical
|
||||
form is `https://{service}.{namespace}:{port}/{path}`.
|
||||
type: string
|
||||
required:
|
||||
- requestType
|
||||
- urlPath
|
||||
- url
|
||||
type: object
|
||||
urlPath:
|
||||
description: URLPath is the URL path to be used in the HTTP
|
||||
GET request to the Kubernetes API server (e.g. "/api/v1/namespaces"
|
||||
or "/apis/apps/v1/deployments"). The format required
|
||||
is the same format used by the `kubectl get --raw` command.
|
||||
GET or POST request to the Kubernetes API server (e.g.
|
||||
"/api/v1/namespaces" or "/apis/apps/v1/deployments").
|
||||
The format required is the same format used by the `kubectl
|
||||
get --raw` command. See https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls
|
||||
for details.
|
||||
type: string
|
||||
type: object
|
||||
configMap:
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -545,9 +545,37 @@ string
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>URLPath is the URL path to be used in the HTTP GET request to the
|
||||
<p>URLPath is the URL path to be used in the HTTP GET or POST request to the
|
||||
Kubernetes API server (e.g. “/api/v1/namespaces” or “/apis/apps/v1/deployments”).
|
||||
The format required is the same format used by the <code>kubectl get --raw</code> command.</p>
|
||||
The format required is the same format used by the <code>kubectl get --raw</code> command.
|
||||
See <a href="https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls">https://kyverno.io/docs/writing-policies/external-data-sources/#variables-from-kubernetes-api-server-calls</a>
|
||||
for details.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>method</code><br/>
|
||||
<em>
|
||||
<a href="#kyverno.io/v1.Method">
|
||||
Method
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Method is the HTTP request type (GET or POST).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>data</code><br/>
|
||||
<em>
|
||||
<a href="#kyverno.io/v1.RequestData">
|
||||
[]RequestData
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data specifies the POST data sent to the server.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -2274,7 +2302,7 @@ Please specify under “any” or “all” instead.</p>
|
|||
(<code>string</code> alias)</p></h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#kyverno.io/v1.ServiceCall">ServiceCall</a>)
|
||||
<a href="#kyverno.io/v1.APICall">APICall</a>)
|
||||
</p>
|
||||
<p>
|
||||
<p>Method is a HTTP request type.</p>
|
||||
|
@ -2587,7 +2615,7 @@ RuleCountStatus
|
|||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#kyverno.io/v1.ServiceCall">ServiceCall</a>)
|
||||
<a href="#kyverno.io/v1.APICall">APICall</a>)
|
||||
</p>
|
||||
<p>
|
||||
<p>RequestData contains the HTTP POST data</p>
|
||||
|
@ -3178,14 +3206,14 @@ string
|
|||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>urlPath</code><br/>
|
||||
<code>url</code><br/>
|
||||
<em>
|
||||
string
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>URL is the JSON web service URL.
|
||||
The typical format is <code>https://{service}.{namespace}:{port}/{path}</code>.</p>
|
||||
<p>URL is the JSON web service URL. A typical form is
|
||||
<code>https://{service}.{namespace}:{port}/{path}</code>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -3200,32 +3228,6 @@ string
|
|||
the server certificate.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>requestType</code><br/>
|
||||
<em>
|
||||
<a href="#kyverno.io/v1.Method">
|
||||
Method
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Method is the HTTP request type (GET or POST).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>data</code><br/>
|
||||
<em>
|
||||
<a href="#kyverno.io/v1.RequestData">
|
||||
[]RequestData
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<p>Data specifies the POST data sent to the server.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
|
||||
|
@ -31,7 +32,7 @@ type Interface interface {
|
|||
// SetDiscovery sets the discovery client implementation
|
||||
SetDiscovery(discoveryClient IDiscovery)
|
||||
// RawAbsPath performs a raw call to the kubernetes API
|
||||
RawAbsPath(ctx context.Context, path string) ([]byte, error)
|
||||
RawAbsPath(ctx context.Context, path string, method string, dataReader io.Reader) ([]byte, error)
|
||||
// GetResource returns the resource in unstructured/json format
|
||||
GetResource(ctx context.Context, apiVersion string, kind string, namespace string, name string, subresources ...string) (*unstructured.Unstructured, error)
|
||||
// PatchResource patches the resource
|
||||
|
@ -141,11 +142,20 @@ func (c *client) GetResource(ctx context.Context, apiVersion string, kind string
|
|||
}
|
||||
|
||||
// RawAbsPath performs a raw call to the kubernetes API
|
||||
func (c *client) RawAbsPath(ctx context.Context, path string) ([]byte, error) {
|
||||
func (c *client) RawAbsPath(ctx context.Context, path string, method string, dataReader io.Reader) ([]byte, error) {
|
||||
if c.rest == nil {
|
||||
return nil, errors.New("rest client not supported")
|
||||
}
|
||||
return c.rest.Get().RequestURI(path).DoRaw(ctx)
|
||||
|
||||
switch method {
|
||||
case "GET":
|
||||
return c.rest.Get().RequestURI(path).DoRaw(ctx)
|
||||
case "POST":
|
||||
return c.rest.Post().Body(dataReader).RequestURI(path).DoRaw(ctx)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("method not supported: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
// PatchResource patches the resource
|
||||
|
|
|
@ -69,33 +69,38 @@ func (a *apiCall) Execute(ctx context.Context) ([]byte, error) {
|
|||
|
||||
func (a *apiCall) execute(ctx context.Context, call *kyvernov1.APICall) ([]byte, error) {
|
||||
if call.URLPath != "" {
|
||||
return a.executeK8sAPICall(ctx, call.URLPath)
|
||||
return a.executeK8sAPICall(ctx, call.URLPath, call.Method, call.Data)
|
||||
}
|
||||
|
||||
return a.executeServiceCall(ctx, call.Service)
|
||||
return a.executeServiceCall(ctx, call)
|
||||
}
|
||||
|
||||
func (a *apiCall) executeK8sAPICall(ctx context.Context, path string) ([]byte, error) {
|
||||
jsonData, err := a.client.RawAbsPath(ctx, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get resource with raw url\n: %s: %v", path, err)
|
||||
}
|
||||
|
||||
a.logger.V(4).Info("executed APICall", "name", a.entry.Name, "len", len(jsonData))
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func (a *apiCall) executeServiceCall(ctx context.Context, service *kyvernov1.ServiceCall) ([]byte, error) {
|
||||
if service == nil {
|
||||
return nil, fmt.Errorf("missing service for APICall %s", a.entry.Name)
|
||||
}
|
||||
|
||||
client, err := a.buildHTTPClient(service)
|
||||
func (a *apiCall) executeK8sAPICall(ctx context.Context, path string, method kyvernov1.Method, data []kyvernov1.RequestData) ([]byte, error) {
|
||||
requestData, err := a.buildRequestData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := a.buildHTTPRequest(ctx, service)
|
||||
jsonData, err := a.client.RawAbsPath(ctx, path, string(method), requestData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to %v resource with raw url\n: %s: %v", method, path, err)
|
||||
}
|
||||
|
||||
a.logger.V(4).Info("executed APICall", "name", a.entry.Name, "path", path, "method", method, "len", len(jsonData))
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func (a *apiCall) executeServiceCall(ctx context.Context, apiCall *kyvernov1.APICall) ([]byte, error) {
|
||||
if apiCall.Service == nil {
|
||||
return nil, fmt.Errorf("missing service for APICall %s", a.entry.Name)
|
||||
}
|
||||
|
||||
client, err := a.buildHTTPClient(apiCall.Service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := a.buildHTTPRequest(ctx, apiCall)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build HTTP request for APICall %s: %w", a.entry.Name, err)
|
||||
}
|
||||
|
@ -124,7 +129,11 @@ func (a *apiCall) executeServiceCall(ctx context.Context, service *kyvernov1.Ser
|
|||
return body, nil
|
||||
}
|
||||
|
||||
func (a *apiCall) buildHTTPRequest(ctx context.Context, service *kyvernov1.ServiceCall) (req *http.Request, err error) {
|
||||
func (a *apiCall) buildHTTPRequest(ctx context.Context, apiCall *kyvernov1.APICall) (req *http.Request, err error) {
|
||||
if apiCall.Service == nil {
|
||||
return nil, fmt.Errorf("missing service")
|
||||
}
|
||||
|
||||
token := a.getToken()
|
||||
defer func() {
|
||||
if token != "" && req != nil {
|
||||
|
@ -132,22 +141,22 @@ func (a *apiCall) buildHTTPRequest(ctx context.Context, service *kyvernov1.Servi
|
|||
}
|
||||
}()
|
||||
|
||||
if service.Method == "GET" {
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", service.URL, nil)
|
||||
if apiCall.Method == "GET" {
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", apiCall.Service.URL, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if service.Method == "POST" {
|
||||
data, dataErr := a.buildPostData(service.Data)
|
||||
if apiCall.Method == "POST" {
|
||||
data, dataErr := a.buildRequestData(apiCall.Data)
|
||||
if dataErr != nil {
|
||||
return nil, dataErr
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("POST", service.URL, data)
|
||||
req, err = http.NewRequest("POST", apiCall.Service.URL, data)
|
||||
return
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid request type %s for APICall %s", service.Method, a.entry.Name)
|
||||
return nil, fmt.Errorf("invalid request type %s for APICall %s", apiCall.Method, a.entry.Name)
|
||||
}
|
||||
|
||||
func (a *apiCall) getToken() string {
|
||||
|
@ -162,7 +171,7 @@ func (a *apiCall) getToken() string {
|
|||
}
|
||||
|
||||
func (a *apiCall) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client, error) {
|
||||
if service.CABundle == "" {
|
||||
if service == nil || service.CABundle == "" {
|
||||
return http.DefaultClient, nil
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
|
@ -180,7 +189,7 @@ func (a *apiCall) buildHTTPClient(service *kyvernov1.ServiceCall) (*http.Client,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (a *apiCall) buildPostData(data []kyvernov1.RequestData) (io.Reader, error) {
|
||||
func (a *apiCall) buildRequestData(data []kyvernov1.RequestData) (io.Reader, error) {
|
||||
dataMap := make(map[string]interface{})
|
||||
for _, d := range data {
|
||||
dataMap[d.Key] = d.Value
|
||||
|
|
|
@ -59,7 +59,7 @@ func Test_serviceGetRequest(t *testing.T) {
|
|||
_, err = call.Execute(context.TODO())
|
||||
assert.ErrorContains(t, err, "invalid request type")
|
||||
|
||||
entry.APICall.Service.Method = "GET"
|
||||
entry.APICall.Method = "GET"
|
||||
call, err = New(logr.Discard(), jp, entry, ctx, nil)
|
||||
assert.NilError(t, err)
|
||||
_, err = call.Execute(context.TODO())
|
||||
|
@ -83,9 +83,9 @@ func Test_servicePostRequest(t *testing.T) {
|
|||
entry := kyvernov1.ContextEntry{
|
||||
Name: "test",
|
||||
APICall: &kyvernov1.APICall{
|
||||
Method: "POST",
|
||||
Service: &kyvernov1.ServiceCall{
|
||||
URL: s.URL + "/resource",
|
||||
Method: "POST",
|
||||
URL: s.URL + "/resource",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ func Test_servicePostRequest(t *testing.T) {
|
|||
err = ctx.AddContextEntry("images", []byte(imageData))
|
||||
assert.NilError(t, err)
|
||||
|
||||
entry.APICall.Service.Data = []kyvernov1.RequestData{
|
||||
entry.APICall.Data = []kyvernov1.RequestData{
|
||||
{
|
||||
Key: "images",
|
||||
Value: &apiextensionsv1.JSON{
|
||||
|
|
|
@ -379,7 +379,11 @@ func Validate(policy, oldPolicy kyvernov1.PolicyInterface, client dclient.Interf
|
|||
allKinds = append(allKinds, matchKinds...)
|
||||
allKinds = append(allKinds, excludeKinds...)
|
||||
if rule.HasValidate() {
|
||||
validationJson, err := json.Marshal(rule.Validation)
|
||||
validationElem := rule.Validation.DeepCopy()
|
||||
if validationElem.Deny != nil {
|
||||
validationElem.Deny.RawAnyAllConditions = nil
|
||||
}
|
||||
validationJson, err := json.Marshal(validationElem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1062,8 +1066,18 @@ func validateConfigMap(entry kyvernov1.ContextEntry) error {
|
|||
}
|
||||
|
||||
func validateAPICall(entry kyvernov1.ContextEntry) error {
|
||||
// If JMESPath contains variables, the validation will fail because it's not possible to infer which value
|
||||
// will be inserted by the variable
|
||||
if entry.APICall == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if entry.APICall.URLPath != "" {
|
||||
if entry.APICall.Service != nil {
|
||||
return fmt.Errorf("a URLPath cannot be used for service API calls")
|
||||
}
|
||||
}
|
||||
|
||||
// If JMESPath contains variables, the validation will fail because it's not
|
||||
// possible to infer which value will be inserted by the variable
|
||||
// Skip validation if a variable is detected
|
||||
|
||||
jmesPath := variables.ReplaceAllVars(entry.APICall.JMESPath, func(s string) string { return "kyvernojmespathvariable" })
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: check-subjectaccessreview
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
|
@ -0,0 +1,81 @@
|
|||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/component: admission-controller
|
||||
app.kubernetes.io/instance: kyverno
|
||||
app.kubernetes.io/part-of: kyverno
|
||||
name: kyverno:subjectaccessreviews
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/component: admission-controller
|
||||
app.kubernetes.io/instance: kyverno
|
||||
app.kubernetes.io/part-of: kyverno
|
||||
name: kyverno:namespace-delete
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- delete
|
||||
resourceNames:
|
||||
- test-sar
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: test-sar
|
||||
---
|
||||
apiVersion: kyverno.io/v2beta1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: check-subjectaccessreview
|
||||
spec:
|
||||
validationFailureAction: Enforce
|
||||
background: false
|
||||
rules:
|
||||
- name: check-sar
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- ConfigMap
|
||||
context:
|
||||
- name: subjectaccessreview
|
||||
apiCall:
|
||||
urlPath: /apis/authorization.k8s.io/v1/subjectaccessreviews
|
||||
method: POST
|
||||
data:
|
||||
- key: kind
|
||||
value: SubjectAccessReview
|
||||
- key: apiVersion
|
||||
value: authorization.k8s.io/v1
|
||||
- key: spec
|
||||
value:
|
||||
resourceAttributes:
|
||||
resource: namespaces
|
||||
name: "{{ request.namespace }}"
|
||||
verb: "delete"
|
||||
group: ""
|
||||
#user: "{{ request.userInfo.username }}"
|
||||
user: "system:serviceaccount:kyverno:kyverno-admission-controller"
|
||||
validate:
|
||||
message: "User is not authorized."
|
||||
deny:
|
||||
conditions:
|
||||
any:
|
||||
- key: "{{ subjectaccessreview.status.allowed }}"
|
||||
operator: NotEquals
|
||||
value: true
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: cm-default-ns.yaml
|
||||
shouldFail: true
|
||||
- file: cm-test-ns.yaml
|
||||
shouldFail: false
|
|
@ -0,0 +1,13 @@
|
|||
## Description
|
||||
|
||||
This test checks a POST operation to the Kubernetes API server for a SubjectAccessReview. It checks for delete access to the namespace of the request, and allows or denies the request.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
The test resource should be allowed to be created in the test namespace but not in the `default` namespace, as Kyverno cannot delete it.
|
||||
|
||||
## Reference Issues
|
||||
|
||||
https://github.com/kyverno/kyverno/issues/1717
|
||||
|
||||
https://github.com/kyverno/kyverno/issues/6857
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm
|
||||
namespace: default
|
||||
data: {}
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm
|
||||
namespace: test-sar
|
||||
data: {}
|
Loading…
Add table
Reference in a new issue