Merge pull request #3802 from tjamet/admission-controller

Add a validating webhook for ingress sanity check
This commit is contained in:
Kubernetes Prow Robot 2019-05-02 07:52:25 -07:00 committed by GitHub
commit b4f2880ee6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3314 additions and 131 deletions

View file

@ -159,6 +159,14 @@ Feature backed by OpenResty Lua libraries. Requires that OCSP stapling is not en
disableCatchAll = flags.Bool("disable-catch-all", false,
`Disable support for catch-all Ingresses`)
validationWebhook = flags.String("validating-webhook", "",
`The address to start an admission controller on to validate incoming ingresses.
Takes the form "<host>:port". If not provided, no admission controller is started.`)
validationWebhookCert = flags.String("validating-webhook-certificate", "",
`The path of the validating webhook certificate PEM.`)
validationWebhookKey = flags.String("validating-webhook-key", "",
`The path of the validating webhook key PEM.`)
)
flags.MarkDeprecated("status-port", `The status port is a unix socket now.`)
@ -255,7 +263,10 @@ Feature backed by OpenResty Lua libraries. Requires that OCSP stapling is not en
HTTPS: *httpsPort,
SSLProxy: *sslProxyPort,
},
DisableCatchAll: *disableCatchAll,
DisableCatchAll: *disableCatchAll,
ValidationWebhook: *validationWebhook,
ValidationWebhookCertPath: *validationWebhookCert,
ValidationWebhookKeyPath: *validationWebhookKey,
}
return false, config, nil

View file

@ -0,0 +1,25 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: check-ingress
webhooks:
- name: validate.nginx.ingress.kubernetes.io
rules:
- apiGroups:
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
failurePolicy: Fail
clientConfig:
service:
namespace: ingress-nginx
name: nginx-ingress-webhook
path: /extensions/v1beta1/ingresses
caBundle: <certificate.pem | base64>
---

View file

@ -0,0 +1,115 @@
---
apiVersion: v1
kind: Service
metadata:
name: ingress-validation-webhook
namespace: ingress-nginx
spec:
ports:
- name: admission
port: 443
protocol: TCP
targetPort: 8080
selector:
app.kubernetes.io/name: ingress-nginx
---
apiVersion: v1
data:
key.pem: <key.pem | base64>
certificate.pem: <certificate.pem | base64>
kind: Secret
metadata:
name: nginx-ingress-webhook-certificate
namespace: ingress-nginx
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller
image: containers.schibsted.io/thibault-jamet/ingress-nginx:0.23.0-schibsted
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=nginx.ingress.kubernetes.io
- --validating-webhook=:8080
- --validating-webhook-certificate=/usr/local/certificates/certificate.pem
- --validating-webhook-key=/usr/local/certificates/key.pem
volumeMounts:
- name: webhook-cert
mountPath: "/usr/local/certificates/"
readOnly: true
securityContext:
allowPrivilegeEscalation: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# www-data -> 33
runAsUser: 33
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
- name: webhook
containerPort: 8080
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
volumes:
- name: webhook-cert
secret:
secretName: nginx-ingress-webhook-certificate
---

View file

@ -0,0 +1,168 @@
# Validating webhook (admission controller)
## Overview
Nginx ingress controller offers the option to validate ingresses before they enter the cluster, ensuring controller will generate a valid configuration.
This controller is called, when [ValidatingAdmissionWebhook][1] is enabled, by the Kubernetes API server each time a new ingress is to enter the cluster, and rejects objects for which the generated nginx configuration fails to be validated.
This feature requires some further configuration of the cluster, hence it is an optional feature, this section explains how to enable it for your cluster.
## Configure the webhook
### Generate the webhook certificate
#### Self signed certificate
Validating webhook must be served using TLS, you need to generate a certificate. Note that kube API server is checking the hostname of the certificate, the common name of your certificate will need to match the service name.
!!! example
To run the validating webhook with a service named `ingress-validation-webhook` in the namespace `ingress-nginx`, run
```bash
openssl req -x509 -newkey rsa:2048 -keyout certificate.pem -out key.pem -days 365 -nodes -subj "/CN=ingress-validation-webhook.ingress-nginx.svc"
```
##### Using Kubernetes CA
Kubernetes also provides primitives to sign a certificate request. Here is an example on how to use it
!!! example
```
#!/bin/bash
SERVICE_NAME=ingress-nginx
NAMESPACE=ingress-nginx
TEMP_DIRECTORY=$(mktemp -d)
echo "creating certs in directory ${TEMP_DIRECTORY}"
cat <<EOF >> ${TEMP_DIRECTORY}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${SERVICE_NAME}
DNS.2 = ${SERVICE_NAME}.${NAMESPACE}
DNS.3 = ${SERVICE_NAME}.${NAMESPACE}.svc
EOF
openssl genrsa -out ${TEMP_DIRECTORY}/server-key.pem 2048
openssl req -new -key ${TEMP_DIRECTORY}/server-key.pem \
-subj "/CN=${SERVICE_NAME}.${NAMESPACE}.svc" \
-out ${TEMP_DIRECTORY}/server.csr \
-config ${TEMP_DIRECTORY}/csr.conf
cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: ${SERVICE_NAME}.${NAMESPACE}.svc
spec:
request: $(cat ${TEMP_DIRECTORY}/server.csr | base64 | tr -d '\n')
usages:
- digital signature
- key encipherment
- server auth
EOF
kubectl certificate approve ${SERVICE_NAME}.${NAMESPACE}.svc
for x in $(seq 10); do
SERVER_CERT=$(kubectl get csr ${SERVICE_NAME}.${NAMESPACE}.svc -o jsonpath='{.status.certificate}')
if [[ ${SERVER_CERT} != '' ]]; then
break
fi
sleep 1
done
if [[ ${SERVER_CERT} == '' ]]; then
echo "ERROR: After approving csr ${SERVICE_NAME}.${NAMESPACE}.svc, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
exit 1
fi
echo ${SERVER_CERT} | openssl base64 -d -A -out ${TEMP_DIRECTORY}/server-cert.pem
kubectl create secret generic ingress-nginx.svc \
--from-file=key.pem=${TEMP_DIRECTORY}/server-key.pem \
--from-file=cert.pem=${TEMP_DIRECTORY}/server-cert.pem \
-n ${NAMESPACE}
```
#### Using helm
To generate the certificate using helm, you can use the following snippet
!!! example
```
{{- $cn := printf "%s.%s.svc" ( include "nginx-ingress.validatingWebhook.fullname" . ) .Release.Namespace }}
{{- $ca := genCA (printf "%s-ca" ( include "nginx-ingress.validatingWebhook.fullname" . )) .Values.validatingWebhook.certificateValidity -}}
{{- $cert := genSignedCert $cn nil nil .Values.validatingWebhook.certificateValidity $ca -}}
```
### Ingress controller flags
To enable the feature in the ingress controller, you _need_ to provide 3 flags to the command line.
|flag|description|example usage|
|-|-|-|
|`--validating-webhook`|The address to start an admission controller on|`:8080`|
|`--validating-webhook-certificate`|The certificate the webhook is using for its TLS handling|`/usr/local/certificates/validating-webhook.pem`|
|`--validating-webhook-key`|The key the webhook is using for its TLS handling|`/usr/local/certificates/validating-webhook-key.pem`|
### kube API server flags
Validating webhook feature requires specific setup on the kube API server side. Depending on your kubernetes version, the flag can, or not, be enabled by default.
To check that your kube API server runs with the required flags, please refer to the [kubernetes][1] documentation.
### Additional kubernetes objects
Once both the ingress controller and the kube API server are configured to serve the webhook, add the you can configure the webhook with the following objects:
```yaml
apiVersion: v1
kind: Service
metadata:
name: ingress-validation-webhook
namespace: ingress-nginx
spec:
ports:
- name: admission
port: 443
protocol: TCP
targetPort: 8080
selector:
app: nginx-ingress
component: controller
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: check-ingress
webhooks:
- name: validate.nginx.ingress.kubernetes.io
rules:
- apiGroups:
- extensions
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- ingresses
failurePolicy: Fail
clientConfig:
service:
namespace: ingress-nginx
name: ingress-validation-webhook
path: /extensions/v1beta1/ingress
caBundle: <pem encoded ca cert that signs the server cert used by the webhook>
```
[1]: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook

View file

@ -56,6 +56,13 @@ On every endpoint change the controller fetches endpoints from all the services
In a relatively big clusters with frequently deploying apps this feature saves significant number of Nginx reloads which can otherwise affect response latency, load balancing quality (after every reload Nginx resets the state of load balancing) and so on.
### Avoiding outage from wrong configuration
Because the ingress controller works using the [synchronization loop pattern](https://coreos.com/kubernetes/docs/latest/replication-controller.html#the-reconciliation-loop-in-detail), it is applying the configuration for all matching objects. In case some Ingress objects have a broken configuration, for example a syntax error in the `nginx.ingress.kubernetes.io/configuration-snippet` annotation, the generated configuration becomes invalid, does not reload and hence no more ingresses will be taken into account.
To prevent this situation to happen, the nginx ingress controller exposes optionnally a [validating admission webhook server][8] to ensure the validity of incoming ingress objects.
This webhook appends the incoming ingress objects to the list of ingresses, generates the configuration and calls nginx to ensure the configuration has no syntax errors.
[0]: https://github.com/openresty/lua-nginx-module/pull/1259
[1]: https://coreos.com/kubernetes/docs/latest/replication-controller.html#the-reconciliation-loop-in-detail
[2]: https://godoc.org/k8s.io/client-go/informers#NewFilteredSharedInformerFactory
@ -64,3 +71,4 @@ In a relatively big clusters with frequently deploying apps this feature saves s
[5]: https://golang.org/pkg/sync/#Mutex
[6]: https://github.com/kubernetes/ingress-nginx/blob/master/rootfs/etc/nginx/template/nginx.tmpl
[7]: http://nginx.org/en/docs/beginners_guide.html#control
[8]: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook

View file

@ -44,3 +44,7 @@ They are set in the container spec of the `nginx-ingress-controller` Deployment
| `--version` | Show release information about the NGINX Ingress controller and exit. |
| `--vmodule moduleSpec` | comma-separated list of pattern=N settings for file-filtered logging |
| `--watch-namespace string` | Namespace the controller watches for updates to Kubernetes objects. This includes Ingresses, Services and all configuration resources. All namespaces are watched if this parameter is left empty. |
| `--disable-catch-all` | Disable support for catch-all Ingresses. |
|`--validating-webhook`|The address to start an admission controller on|
|`--validating-webhook-certificate`|The certificate the webhook is using for its TLS handling|
|`--validating-webhook-key`|The key the webhook is using for its TLS handling|

4
go.mod
View file

@ -19,8 +19,10 @@ require (
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect
github.com/go-openapi/spec v0.19.0 // indirect
github.com/gofortune/gofortune v0.0.1-snapshot // indirect
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/google/gofuzz v1.0.0 // indirect
github.com/google/uuid v1.0.0
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gophercloud/gophercloud v0.0.0-20190410012400-2c55d17f707c // indirect
github.com/imdario/mergo v0.3.7
@ -54,7 +56,9 @@ require (
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926
github.com/vromero/gofortune v0.0.1-snapshot
github.com/zakjan/cert-chain-resolver v0.0.0-20180703112424-6076e1ded272
google.golang.org/grpc v1.19.1
gopkg.in/fsnotify/fsnotify.v1 v1.4.7
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/pool.v3 v3.1.1

4
go.sum
View file

@ -72,6 +72,8 @@ github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsd
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofortune/gofortune v0.0.1-snapshot h1:0unUpPzS0PAdMrOvLAhmeaGtFlUPYv5aXUD/9XN5X9U=
github.com/gofortune/gofortune v0.0.1-snapshot/go.mod h1:gzHWMyrWq6g1heq6667VSJTUxWXv+9mTry2HjUnEVB4=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -234,6 +236,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/vromero/gofortune v0.0.1-snapshot h1:+IDjezRGmRO1Mdm1Oh+DguaSkxjRpoevWlpdTSlwPkw=
github.com/vromero/gofortune v0.0.1-snapshot/go.mod h1:t8EOM3RyBWLevtrXkmQtfAMmH5CU3/YcnpG5RZ/GQXQ=
github.com/zakjan/cert-chain-resolver v0.0.0-20180703112424-6076e1ded272 h1:scDk3LAM8x+NPuywVGC0q0/G+5Ed8M0+YXecz4XnWRM=
github.com/zakjan/cert-chain-resolver v0.0.0-20180703112424-6076e1ded272/go.mod h1:KNkcm66cr4ilOiEcjydK+tc2ShPUhqmuoXCljXUBPu8=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=

View file

@ -0,0 +1,93 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"github.com/google/uuid"
"k8s.io/api/admission/v1beta1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
"k8s.io/klog"
)
// Checker must return an error if the ingress provided as argument
// contains invalid instructions
type Checker interface {
CheckIngress(ing *extensions.Ingress) error
}
// IngressAdmission implements the AdmissionController interface
// to handle Admission Reviews and deny requests that are not validated
type IngressAdmission struct {
Checker Checker
}
// HandleAdmission populates the admission Response
// with Allowed=false if the Object is an ingress that would prevent nginx to reload the configuration
// with Allowed=true otherwise
func (ia *IngressAdmission) HandleAdmission(ar *v1beta1.AdmissionReview) error {
if ar.Request == nil {
klog.Infof("rejecting nil request")
ar.Response = &v1beta1.AdmissionResponse{
UID: types.UID(uuid.New().String()),
Allowed: false,
}
return nil
}
klog.V(3).Infof("handling ingress admission webhook request for {%s} %s in namespace %s", ar.Request.Resource.String(), ar.Request.Name, ar.Request.Namespace)
ingressResource := v1.GroupVersionResource{Group: extensions.SchemeGroupVersion.Group, Version: extensions.SchemeGroupVersion.Version, Resource: "ingresses"}
if ar.Request.Resource == ingressResource {
ar.Response = &v1beta1.AdmissionResponse{
UID: types.UID(uuid.New().String()),
Allowed: false,
}
ingress := extensions.Ingress{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(ar.Request.Object.Raw, nil, &ingress); err != nil {
ar.Response.Result = &v1.Status{Message: err.Error()}
ar.Response.AuditAnnotations = map[string]string{
parser.GetAnnotationWithPrefix("error"): err.Error(),
}
klog.Errorf("failed to decode ingress %s in namespace %s: %s, refusing it", ar.Request.Name, ar.Request.Namespace, err.Error())
return err
}
err := ia.Checker.CheckIngress(&ingress)
if err != nil {
ar.Response.Result = &v1.Status{Message: err.Error()}
ar.Response.AuditAnnotations = map[string]string{
parser.GetAnnotationWithPrefix("error"): err.Error(),
}
klog.Errorf("failed to generate configuration for ingress %s in namespace %s: %s, refusing it", ar.Request.Name, ar.Request.Namespace, err.Error())
return err
}
ar.Response.Allowed = true
klog.Infof("successfully validated configuration, accepting ingress %s in namespace %s", ar.Request.Name, ar.Request.Namespace)
return nil
}
klog.Infof("accepting non ingress %s in namespace %s %s", ar.Request.Name, ar.Request.Namespace, ar.Request.Resource.String())
ar.Response = &v1beta1.AdmissionResponse{
UID: types.UID(uuid.New().String()),
Allowed: true,
}
return nil
}

View file

@ -0,0 +1,109 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"fmt"
"testing"
"k8s.io/api/admission/v1beta1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/json"
)
const testIngressName = "testIngressName"
type failTestChecker struct {
t *testing.T
}
func (ftc failTestChecker) CheckIngress(ing *extensions.Ingress) error {
ftc.t.Error("checker should not be called")
return nil
}
type testChecker struct {
t *testing.T
err error
}
func (tc testChecker) CheckIngress(ing *extensions.Ingress) error {
if ing.ObjectMeta.Name != testIngressName {
tc.t.Errorf("CheckIngress should be called with %v ingress, but got %v", testIngressName, ing.ObjectMeta.Name)
}
return tc.err
}
func TestHandleAdmission(t *testing.T) {
adm := &IngressAdmission{
Checker: failTestChecker{t: t},
}
review := &v1beta1.AdmissionReview{
Request: &v1beta1.AdmissionRequest{
Resource: v1.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"},
},
}
err := adm.HandleAdmission(review)
if !review.Response.Allowed {
t.Errorf("with a non ingress resource, the check should pass")
}
if err != nil {
t.Errorf("with a non ingress resource, no error should be returned")
}
review.Request.Resource = v1.GroupVersionResource{Group: extensions.SchemeGroupVersion.Group, Version: extensions.SchemeGroupVersion.Version, Resource: "ingresses"}
review.Request.Object.Raw = []byte{0xff}
err = adm.HandleAdmission(review)
if review.Response.Allowed {
t.Errorf("when the request object is not decodable, the request should not be allowed")
}
if err == nil {
t.Errorf("when the request object is not decodable, an error should be returned")
}
raw, err := json.Marshal(extensions.Ingress{ObjectMeta: v1.ObjectMeta{Name: testIngressName}})
if err != nil {
t.Errorf("failed to prepare test ingress data: %v", err.Error())
}
review.Request.Object.Raw = raw
adm.Checker = testChecker{
t: t,
err: fmt.Errorf("this is a test error"),
}
err = adm.HandleAdmission(review)
if review.Response.Allowed {
t.Errorf("when the checker returns an error, the request should not be allowed")
}
if err == nil {
t.Errorf("when the checker returns an error, an error should be returned")
}
adm.Checker = testChecker{
t: t,
err: nil,
}
err = adm.HandleAdmission(review)
if !review.Response.Allowed {
t.Errorf("when the checker returns no error, the request should be allowed")
}
if err != nil {
t.Errorf("when the checker returns no error, no error should be returned")
}
}

View file

@ -0,0 +1,89 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"io"
"io/ioutil"
"net/http"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/klog"
)
var (
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
// AdmissionController checks if an object
// is allowed in the cluster
type AdmissionController interface {
HandleAdmission(*v1beta1.AdmissionReview) error
}
// AdmissionControllerServer implements an HTTP server
// for kubernetes validating webhook
// https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook
type AdmissionControllerServer struct {
AdmissionController AdmissionController
Decoder runtime.Decoder
}
// NewAdmissionControllerServer instanciates an admission controller server with
// a default codec
func NewAdmissionControllerServer(ac AdmissionController) *AdmissionControllerServer {
return &AdmissionControllerServer{
AdmissionController: ac,
Decoder: codecs.UniversalDeserializer(),
}
}
// ServeHTTP implements http.Server method
func (acs *AdmissionControllerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
klog.Infof("handling admission controller request %s", r.URL.String())
review, err := parseAdmissionReview(acs.Decoder, r.Body)
if err != nil {
klog.Error("Can't decode request", err)
w.WriteHeader(http.StatusBadRequest)
return
}
acs.AdmissionController.HandleAdmission(review)
if err := writeAdmissionReview(w, review); err != nil {
klog.Error(err)
}
}
func parseAdmissionReview(decoder runtime.Decoder, r io.Reader) (*v1beta1.AdmissionReview, error) {
review := &v1beta1.AdmissionReview{}
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
_, _, err = decoder.Decode(data, nil, review)
return review, err
}
func writeAdmissionReview(w io.Writer, ar *v1beta1.AdmissionReview) error {
e := json.NewEncoder(w)
return e.Encode(ar)
}

View file

@ -0,0 +1,97 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/uuid"
"k8s.io/api/admission/v1beta1"
"k8s.io/apimachinery/pkg/types"
)
type testAdmissionHandler struct{}
func (testAdmissionHandler) HandleAdmission(ar *v1beta1.AdmissionReview) error {
ar.Response = &v1beta1.AdmissionResponse{
UID: types.UID(uuid.New().String()),
Allowed: true,
}
return nil
}
type errorReader struct{}
func (errorReader) Read(p []byte) (n int, err error) {
return 0, fmt.Errorf("this is a test error")
}
type errorWriter struct{}
func (errorWriter) Write(p []byte) (n int, err error) {
return 0, fmt.Errorf("this is a test error")
}
func (errorWriter) Header() http.Header {
return nil
}
func (errorWriter) WriteHeader(statusCode int) {}
func TestServer(t *testing.T) {
w := httptest.NewRecorder()
b := bytes.NewBuffer(nil)
writeAdmissionReview(b, &v1beta1.AdmissionReview{})
// Happy path
r := httptest.NewRequest("GET", "http://test.ns.svc", b)
NewAdmissionControllerServer(testAdmissionHandler{}).ServeHTTP(w, r)
ar, err := parseAdmissionReview(codecs.UniversalDeserializer(), w.Body)
if w.Code != http.StatusOK {
t.Errorf("when the admission review allows the request, the http status should be OK")
}
if err != nil {
t.Errorf("failed to parse admission response when the admission controller returns a value")
}
if !ar.Response.Allowed {
t.Errorf("when the admission review allows the request, the parsed body returns not allowed")
}
// Ensure the code does not panic when failing to handle the request
NewAdmissionControllerServer(testAdmissionHandler{}).ServeHTTP(errorWriter{}, r)
w = httptest.NewRecorder()
NewAdmissionControllerServer(testAdmissionHandler{}).ServeHTTP(w, httptest.NewRequest("GET", "http://test.ns.svc", strings.NewReader("invalid-json")))
if w.Code != http.StatusBadRequest {
t.Errorf("when the server fails to read the request, the replied status should be bad request")
}
}
func TestParseAdmissionReview(t *testing.T) {
ar, err := parseAdmissionReview(codecs.UniversalDeserializer(), errorReader{})
if ar != nil {
t.Errorf("when reading from request fails, no AdmissionRewiew should be returned")
}
if err == nil {
t.Errorf("when reading from request fails, an error should be returned")
}
}

View file

@ -23,23 +23,21 @@ import (
"strings"
"time"
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
"github.com/mitchellh/hashstructure"
"k8s.io/klog"
apiv1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/ingress/annotations"
"k8s.io/ingress-nginx/internal/ingress/annotations/class"
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
"k8s.io/ingress-nginx/internal/ingress/annotations/proxy"
ngx_config "k8s.io/ingress-nginx/internal/ingress/controller/config"
"k8s.io/ingress-nginx/internal/k8s"
"k8s.io/klog"
)
const (
@ -96,6 +94,10 @@ type Configuration struct {
DynamicCertificatesEnabled bool
DisableCatchAll bool
ValidationWebhook string
ValidationWebhookCertPath string
ValidationWebhookKeyPath string
}
// GetPublishService returns the Service used to set the load-balancer status of Ingresses.
@ -120,47 +122,7 @@ func (n *NGINXController) syncIngress(interface{}) error {
ings := n.store.ListIngresses()
upstreams, servers := n.getBackendServers(ings)
var passUpstreams []*ingress.SSLPassthroughBackend
hosts := sets.NewString()
for _, server := range servers {
if !hosts.Has(server.Hostname) {
hosts.Insert(server.Hostname)
}
if server.Alias != "" && !hosts.Has(server.Alias) {
hosts.Insert(server.Alias)
}
if !server.SSLPassthrough {
continue
}
for _, loc := range server.Locations {
if loc.Path != rootLocation {
klog.Warningf("Ignoring SSL Passthrough for location %q in server %q", loc.Path, server.Hostname)
continue
}
passUpstreams = append(passUpstreams, &ingress.SSLPassthroughBackend{
Backend: loc.Backend,
Hostname: server.Hostname,
Service: loc.Service,
Port: loc.Port,
})
break
}
}
pcfg := &ingress.Configuration{
Backends: upstreams,
Servers: servers,
TCPEndpoints: n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
UDPEndpoints: n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
PassthroughBackends: passUpstreams,
BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
ControllerPodsCount: n.store.GetRunningControllerPodsCount(),
}
hosts, servers, pcfg := n.getConfiguration(ings)
if n.runningConfig.Equal(pcfg) {
klog.V(3).Infof("No configuration change detected, skipping backend reload.")
@ -235,6 +197,60 @@ func (n *NGINXController) syncIngress(interface{}) error {
return nil
}
// CheckIngress returns an error in case the provided ingress, when added
// to the current configuration, generates an invalid configuration
func (n *NGINXController) CheckIngress(ing *extensions.Ingress) error {
if n == nil {
return fmt.Errorf("cannot check ingress on a nil ingress controller")
}
if ing == nil {
// no ingress to add, no state change
return nil
}
if !class.IsValid(ing) {
klog.Infof("ignoring ingress %v in %v based on annotation %v", ing.Name, ing.ObjectMeta.Namespace, class.IngressKey)
return nil
}
if n.cfg.Namespace != "" && ing.ObjectMeta.Namespace != n.cfg.Namespace {
klog.Infof("ignoring ingress %v in namespace %v different from the namespace watched %s", ing.Name, ing.ObjectMeta.Namespace, n.cfg.Namespace)
return nil
}
ings := n.store.ListIngresses()
newIngress := &ingress.Ingress{
Ingress: *ing,
ParsedAnnotations: annotations.NewAnnotationExtractor(n.store).Extract(ing),
}
for i, ingress := range ings {
if ingress.Ingress.ObjectMeta.Name == ing.ObjectMeta.Name && ingress.Ingress.ObjectMeta.Namespace == ing.ObjectMeta.Namespace {
ings[i] = newIngress
newIngress = nil
}
}
if newIngress != nil {
ings = append(ings, newIngress)
}
_, _, pcfg := n.getConfiguration(ings)
cfg := n.store.GetBackendConfiguration()
cfg.Resolver = n.resolver
content, err := n.generateTemplate(cfg, *pcfg)
if err != nil {
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
return err
}
err = n.testTemplate(content)
if err != nil {
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
} else {
n.metricCollector.IncCheckCount(ing.ObjectMeta.Namespace, ing.Name)
}
return err
}
func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Protocol) []ingress.L4Service {
if configmapName == "" {
return []ingress.L4Service{}
@ -380,6 +396,51 @@ func (n *NGINXController) getDefaultUpstream() *ingress.Backend {
return upstream
}
// getConfiguration returns the configuration matching the standard kubernetes ingress
func (n *NGINXController) getConfiguration(ingresses []*ingress.Ingress) (sets.String, []*ingress.Server, *ingress.Configuration) {
upstreams, servers := n.getBackendServers(ingresses)
var passUpstreams []*ingress.SSLPassthroughBackend
hosts := sets.NewString()
for _, server := range servers {
if !hosts.Has(server.Hostname) {
hosts.Insert(server.Hostname)
}
if server.Alias != "" && !hosts.Has(server.Alias) {
hosts.Insert(server.Alias)
}
if !server.SSLPassthrough {
continue
}
for _, loc := range server.Locations {
if loc.Path != rootLocation {
klog.Warningf("Ignoring SSL Passthrough for location %q in server %q", loc.Path, server.Hostname)
continue
}
passUpstreams = append(passUpstreams, &ingress.SSLPassthroughBackend{
Backend: loc.Backend,
Hostname: server.Hostname,
Service: loc.Service,
Port: loc.Port,
})
break
}
}
return hosts, servers, &ingress.Configuration{
Backends: upstreams,
Servers: servers,
TCPEndpoints: n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
UDPEndpoints: n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
PassthroughBackends: passUpstreams,
BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
ControllerPodsCount: n.store.GetRunningControllerPodsCount(),
}
}
// getBackendServers returns a list of Upstream and Server to be used by the
// backend. An upstream can be used in multiple servers if the namespace,
// service name and port are the same.

View file

@ -21,12 +21,17 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"time"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/eapache/channels"
"k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
@ -35,14 +40,216 @@ import (
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/ingress/annotations"
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
"k8s.io/ingress-nginx/internal/ingress/controller/config"
ngx_config "k8s.io/ingress-nginx/internal/ingress/controller/config"
"k8s.io/ingress-nginx/internal/ingress/controller/store"
"k8s.io/ingress-nginx/internal/ingress/defaults"
"k8s.io/ingress-nginx/internal/ingress/metric"
"k8s.io/ingress-nginx/internal/ingress/resolver"
"k8s.io/ingress-nginx/internal/k8s"
"k8s.io/ingress-nginx/internal/net/ssl"
)
const fakeCertificateName = "default-fake-certificate"
type fakeIngressStore struct {
ingresses []*ingress.Ingress
}
func (fakeIngressStore) GetBackendConfiguration() ngx_config.Configuration {
return ngx_config.Configuration{}
}
func (fakeIngressStore) GetConfigMap(key string) (*corev1.ConfigMap, error) {
return nil, fmt.Errorf("test error")
}
func (fakeIngressStore) GetSecret(key string) (*corev1.Secret, error) {
return nil, fmt.Errorf("test error")
}
func (fakeIngressStore) GetService(key string) (*corev1.Service, error) {
return nil, fmt.Errorf("test error")
}
func (fakeIngressStore) GetServiceEndpoints(key string) (*corev1.Endpoints, error) {
return nil, fmt.Errorf("test error")
}
func (fis fakeIngressStore) ListIngresses() []*ingress.Ingress {
return fis.ingresses
}
func (fakeIngressStore) GetRunningControllerPodsCount() int {
return 0
}
func (fakeIngressStore) GetLocalSSLCert(name string) (*ingress.SSLCert, error) {
return nil, fmt.Errorf("test error")
}
func (fakeIngressStore) ListLocalSSLCerts() []*ingress.SSLCert {
return nil
}
func (fakeIngressStore) GetAuthCertificate(string) (*resolver.AuthSSLCert, error) {
return nil, fmt.Errorf("test error")
}
func (fakeIngressStore) GetDefaultBackend() defaults.Backend {
return defaults.Backend{}
}
func (fakeIngressStore) Run(stopCh chan struct{}) {}
type testNginxTestCommand struct {
t *testing.T
expected string
out []byte
err error
}
func (ntc testNginxTestCommand) ExecCommand(args ...string) *exec.Cmd {
return nil
}
func (ntc testNginxTestCommand) Test(cfg string) ([]byte, error) {
fd, err := os.Open(cfg)
if err != nil {
ntc.t.Errorf("could not read generated nginx configuration: %v", err.Error())
return nil, err
}
defer fd.Close()
bytes, err := ioutil.ReadAll(fd)
if err != nil {
ntc.t.Errorf("could not read generated nginx configuration: %v", err.Error())
}
if string(bytes) != ntc.expected {
ntc.t.Errorf("unexpected generated configuration %v. Expecting %v", string(bytes), ntc.expected)
}
return ntc.out, ntc.err
}
type fakeTemplate struct{}
func (fakeTemplate) Write(conf config.TemplateConfig) ([]byte, error) {
r := []byte{}
for _, s := range conf.Servers {
if len(r) > 0 {
r = append(r, ',')
}
r = append(r, []byte(s.Hostname)...)
}
return r, nil
}
func TestCheckIngress(t *testing.T) {
defer func() {
filepath.Walk(os.TempDir(), func(path string, info os.FileInfo, err error) error {
if info.IsDir() && os.TempDir() != path {
return filepath.SkipDir
}
if strings.HasPrefix(info.Name(), tempNginxPattern) {
os.Remove(path)
}
return nil
})
}()
// Ensure no panic with wrong arguments
var nginx *NGINXController
nginx.CheckIngress(nil)
nginx = newNGINXController(t)
nginx.CheckIngress(nil)
nginx.metricCollector = metric.DummyCollector{}
nginx.t = fakeTemplate{}
nginx.store = fakeIngressStore{
ingresses: []*ingress.Ingress{},
}
ing := &extensions.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "user-namespace",
Annotations: map[string]string{},
},
Spec: extensions.IngressSpec{
Rules: []extensions.IngressRule{
{
Host: "example.com",
},
},
},
}
t.Run("When the ingress class differs from nginx", func(t *testing.T) {
ing.ObjectMeta.Annotations["kubernetes.io/ingress.class"] = "different"
nginx.command = testNginxTestCommand{
t: t,
err: fmt.Errorf("test error"),
}
if nginx.CheckIngress(ing) != nil {
t.Errorf("with a different ingress class, no error should be returned")
}
})
t.Run("when the class is the nginx one", func(t *testing.T) {
ing.ObjectMeta.Annotations["kubernetes.io/ingress.class"] = "nginx"
nginx.command = testNginxTestCommand{
t: t,
err: nil,
expected: "_,example.com",
}
if nginx.CheckIngress(ing) != nil {
t.Errorf("with a new ingress without error, no error should be returned")
}
t.Run("When the hostname is updated", func(t *testing.T) {
nginx.store = fakeIngressStore{
ingresses: []*ingress.Ingress{
{
Ingress: *ing,
},
},
}
ing.Spec.Rules[0].Host = "test.example.com"
nginx.command = testNginxTestCommand{
t: t,
err: nil,
expected: "_,test.example.com",
}
if nginx.CheckIngress(ing) != nil {
t.Errorf("with a new ingress without error, no error should be returned")
}
})
t.Run("When nginx test returns an error", func(t *testing.T) {
nginx.command = testNginxTestCommand{
t: t,
err: fmt.Errorf("test error"),
out: []byte("this is the test command output"),
expected: "_,test.example.com",
}
if nginx.CheckIngress(ing) == nil {
t.Errorf("with a new ingress with an error, an error should be returned")
}
})
t.Run("When the ingress is in a different namespace than the watched one", func(t *testing.T) {
nginx.command = testNginxTestCommand{
t: t,
err: fmt.Errorf("test error"),
}
nginx.cfg.Namespace = "other-namespace"
ing.ObjectMeta.Namespace = "test-namespace"
if nginx.CheckIngress(ing) != nil {
t.Errorf("with a new ingress without error, no error should be returned")
}
})
})
}
func TestMergeAlternativeBackends(t *testing.T) {
testCases := map[string]struct {
ingress *ingress.Ingress
@ -930,8 +1137,10 @@ func newNGINXController(t *testing.T) *NGINXController {
}
return &NGINXController{
store: storer,
cfg: config,
store: storer,
cfg: config,
command: NewNginxCommand(),
fileSystem: fs,
}
}

View file

@ -45,9 +45,7 @@ import (
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/flowcontrol"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/util/filesystem"
adm_controler "k8s.io/ingress-nginx/internal/admission/controller"
"k8s.io/ingress-nginx/internal/file"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/ingress/annotations/class"
@ -64,6 +62,8 @@ import (
"k8s.io/ingress-nginx/internal/nginx"
"k8s.io/ingress-nginx/internal/task"
"k8s.io/ingress-nginx/internal/watch"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/util/filesystem"
)
const (
@ -110,6 +110,16 @@ func NewNGINXController(config *Configuration, mc metric.Collector, fs file.File
Proxy: &TCPProxy{},
metricCollector: mc,
command: NewNginxCommand(),
}
if n.cfg.ValidationWebhook != "" {
n.validationWebhookServer = &http.Server{
Addr: config.ValidationWebhook,
Handler: adm_controler.NewAdmissionControllerServer(&adm_controler.IngressAdmission{Checker: n}),
TLSConfig: ssl.NewTLSListener(n.cfg.ValidationWebhookCertPath, n.cfg.ValidationWebhookKeyPath).TLSConfig(),
}
}
pod, err := k8s.GetPodDetails(config.Client)
@ -241,7 +251,7 @@ type NGINXController struct {
// runningConfig contains the running configuration in the Backend
runningConfig *ingress.Configuration
t *ngx_template.Template
t ngx_template.TemplateWriter
resolver []net.IP
@ -258,6 +268,10 @@ type NGINXController struct {
metricCollector metric.Collector
currentLeader uint32
validationWebhookServer *http.Server
command NginxExecTester
}
// Start starts a new NGINX master process running in the foreground.
@ -295,7 +309,7 @@ func (n *NGINXController) Start() {
PodNamespace: n.podInfo.Namespace,
})
cmd := nginxExecCommand()
cmd := n.command.ExecCommand()
// put NGINX in another process group to prevent it
// to receive signals meant for the controller
@ -327,6 +341,13 @@ func (n *NGINXController) Start() {
}
}()
if n.validationWebhookServer != nil {
klog.Infof("Starting validation webhook on %s with keys %s %s", n.validationWebhookServer.Addr, n.cfg.ValidationWebhookCertPath, n.cfg.ValidationWebhookKeyPath)
go func() {
klog.Error(n.validationWebhookServer.ListenAndServeTLS("", ""))
}()
}
for {
select {
case err := <-n.ngxErrCh:
@ -344,7 +365,7 @@ func (n *NGINXController) Start() {
// release command resources
cmd.Process.Release()
// start a new nginx master process if the controller is not being stopped
cmd = nginxExecCommand()
cmd = n.command.ExecCommand()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
@ -391,9 +412,17 @@ func (n *NGINXController) Stop() error {
n.syncStatus.Shutdown()
}
if n.validationWebhookServer != nil {
klog.Info("Stopping admission controller")
err := n.validationWebhookServer.Close()
if err != nil {
return err
}
}
// send stop signal to NGINX
klog.Info("Stopping NGINX process")
cmd := nginxExecCommand("-s", "quit")
cmd := n.command.ExecCommand("-s", "quit")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
@ -437,45 +466,8 @@ func (n NGINXController) DefaultEndpoint() ingress.Endpoint {
}
}
// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n NGINXController) testTemplate(cfg []byte) error {
if len(cfg) == 0 {
return fmt.Errorf("invalid NGINX configuration (empty)")
}
tmpfile, err := ioutil.TempFile("", tempNginxPattern)
if err != nil {
return err
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
if err != nil {
return err
}
out, err := nginxTestCommand(tmpfile.Name()).CombinedOutput()
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
oe := fmt.Sprintf(`
-------------------------------------------------------------------------------
Error: %v
%v
-------------------------------------------------------------------------------
`, err, string(out))
return errors.New(oe)
}
os.Remove(tmpfile.Name())
return nil
}
// OnUpdate is called by the synchronization loop whenever configuration
// changes were detected. The received backend Configuration is merged with the
// configuration ConfigMap before generating the final configuration file.
// Returns nil in case the backend was successfully reloaded.
func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
cfg := n.store.GetBackendConfiguration()
cfg.Resolver = n.resolver
// generateTemplate returns the nginx configuration file content
func (n NGINXController) generateTemplate(cfg ngx_config.Configuration, ingressCfg ingress.Configuration) ([]byte, error) {
if n.cfg.EnableSSLPassthrough {
servers := []*TCPServer{}
@ -638,7 +630,50 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
tc.Cfg.Checksum = ingressCfg.ConfigurationChecksum
content, err := n.t.Write(tc)
return n.t.Write(tc)
}
// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n NGINXController) testTemplate(cfg []byte) error {
if len(cfg) == 0 {
return fmt.Errorf("invalid NGINX configuration (empty)")
}
tmpfile, err := ioutil.TempFile("", tempNginxPattern)
if err != nil {
return err
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
if err != nil {
return err
}
out, err := n.command.Test(tmpfile.Name())
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
oe := fmt.Sprintf(`
-------------------------------------------------------------------------------
Error: %v
%v
-------------------------------------------------------------------------------
`, err, string(out))
return errors.New(oe)
}
os.Remove(tmpfile.Name())
return nil
}
// OnUpdate is called by the synchronization loop whenever configuration
// changes were detected. The received backend Configuration is merged with the
// configuration ConfigMap before generating the final configuration file.
// Returns nil in case the backend was successfully reloaded.
func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
cfg := n.store.GetBackendConfiguration()
cfg.Resolver = n.resolver
content, err := n.generateTemplate(cfg, ingressCfg)
if err != nil {
return err
}
@ -686,7 +721,7 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
return err
}
o, err := nginxExecCommand("-s", "reload").CombinedOutput()
o, err := n.command.ExecCommand("-s", "reload").CombinedOutput()
if err != nil {
return fmt.Errorf("%v\n%v", err, string(o))
}

View file

@ -51,6 +51,11 @@ const (
defBufferSize = 65535
)
// TemplateWriter is the interface to render a template
type TemplateWriter interface {
Write(conf config.TemplateConfig) ([]byte, error)
}
// Template ...
type Template struct {
tmpl *text_template.Template

View file

@ -21,13 +21,11 @@ import (
"os/exec"
"syscall"
"k8s.io/klog"
api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kubernetes/pkg/util/sysctl"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/klog"
"k8s.io/kubernetes/pkg/util/sysctl"
)
// newUpstream creates an upstream without servers.
@ -79,14 +77,36 @@ const (
cfgPath = "/etc/nginx/nginx.conf"
)
func nginxExecCommand(args ...string) *exec.Cmd {
// NginxExecTester defines the interface to execute
// command like reload or test configuration
type NginxExecTester interface {
ExecCommand(args ...string) *exec.Cmd
Test(cfg string) ([]byte, error)
}
// NginxCommand stores context around a given nginx executable path
type NginxCommand struct {
Binary string
}
// NewNginxCommand returns a new NginxCommand from which path
// has been detected from environment variable NGINX_BINARY or default
func NewNginxCommand() NginxCommand {
return NginxCommand{
Binary: defBinary,
}
}
// ExecCommand instanciates an exec.Cmd object to call nginx program
func (nc NginxCommand) ExecCommand(args ...string) *exec.Cmd {
cmdArgs := []string{}
cmdArgs = append(cmdArgs, "-c", cfgPath)
cmdArgs = append(cmdArgs, args...)
return exec.Command(defBinary, cmdArgs...)
return exec.Command(nc.Binary, cmdArgs...)
}
func nginxTestCommand(cfg string) *exec.Cmd {
return exec.Command(defBinary, "-c", cfg, "-t")
// Test checks if config file is a syntax valid nginx configuration
func (nc NginxCommand) Test(cfg string) ([]byte, error) {
return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput()
}

View file

@ -21,15 +21,15 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/klog"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/klog"
)
var (
operation = []string{"controller_namespace", "controller_class", "controller_pod"}
sslLabelHost = []string{"namespace", "class", "host"}
operation = []string{"controller_namespace", "controller_class", "controller_pod"}
ingressOperation = []string{"controller_namespace", "controller_class", "controller_pod", "namespace", "ingress"}
sslLabelHost = []string{"namespace", "class", "host"}
)
// Controller defines base metrics about the ingress controller
@ -40,9 +40,11 @@ type Controller struct {
configSuccess prometheus.Gauge
configSuccessTime prometheus.Gauge
reloadOperation *prometheus.CounterVec
reloadOperationErrors *prometheus.CounterVec
sslExpireTime *prometheus.GaugeVec
reloadOperation *prometheus.CounterVec
reloadOperationErrors *prometheus.CounterVec
checkIngressOperation *prometheus.CounterVec
checkIngressOperationErrors *prometheus.CounterVec
sslExpireTime *prometheus.GaugeVec
constLabels prometheus.Labels
labels prometheus.Labels
@ -105,6 +107,22 @@ func NewController(pod, namespace, class string) *Controller {
},
operation,
),
checkIngressOperationErrors: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: PrometheusNamespace,
Name: "check_errors",
Help: `Cumulative number of Ingress controller errors during syntax check operations`,
},
ingressOperation,
),
checkIngressOperation: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: PrometheusNamespace,
Name: "check_success",
Help: `Cumulative number of Ingress controller syntax check operations`,
},
ingressOperation,
),
sslExpireTime: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: PrometheusNamespace,
@ -148,6 +166,24 @@ func (cm *Controller) OnStoppedLeading(electionID string) {
cm.leaderElection.WithLabelValues(electionID).Set(0)
}
// IncCheckCount increment the check counter
func (cm *Controller) IncCheckCount(namespace, name string) {
labels := prometheus.Labels{
"namespace": namespace,
"ingress": name,
}
cm.checkIngressOperation.MustCurryWith(cm.constLabels).With(labels).Inc()
}
// IncCheckErrorCount increment the check error counter
func (cm *Controller) IncCheckErrorCount(namespace, name string) {
labels := prometheus.Labels{
"namespace": namespace,
"ingress": name,
}
cm.checkIngressOperationErrors.MustCurryWith(cm.constLabels).With(labels).Inc()
}
// ConfigSuccess set a boolean flag according to the output of the controller configuration reload
func (cm *Controller) ConfigSuccess(hash uint64, success bool) {
if success {
@ -170,6 +206,8 @@ func (cm Controller) Describe(ch chan<- *prometheus.Desc) {
cm.configSuccessTime.Describe(ch)
cm.reloadOperation.Describe(ch)
cm.reloadOperationErrors.Describe(ch)
cm.checkIngressOperation.Describe(ch)
cm.checkIngressOperationErrors.Describe(ch)
cm.sslExpireTime.Describe(ch)
cm.leaderElection.Describe(ch)
}
@ -181,6 +219,8 @@ func (cm Controller) Collect(ch chan<- prometheus.Metric) {
cm.configSuccessTime.Collect(ch)
cm.reloadOperation.Collect(ch)
cm.reloadOperationErrors.Collect(ch)
cm.checkIngressOperation.Collect(ch)
cm.checkIngressOperationErrors.Collect(ch)
cm.sslExpireTime.Collect(ch)
cm.leaderElection.Collect(ch)
}

View file

@ -38,6 +38,12 @@ func (dc DummyCollector) IncReloadCount() {}
// IncReloadErrorCount ...
func (dc DummyCollector) IncReloadErrorCount() {}
// IncCheckCount ...
func (dc DummyCollector) IncCheckCount(string, string) {}
// IncCheckErrorCount ...
func (dc DummyCollector) IncCheckErrorCount(string, string) {}
// RemoveMetrics ...
func (dc DummyCollector) RemoveMetrics(ingresses, endpoints []string) {}

View file

@ -21,9 +21,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/ingress/annotations/class"
"k8s.io/ingress-nginx/internal/ingress/metric/collectors"
@ -39,6 +37,9 @@ type Collector interface {
OnStartedLeading(string)
OnStoppedLeading(string)
IncCheckCount(string, string)
IncCheckErrorCount(string, string)
RemoveMetrics(ingresses, endpoints []string)
SetSSLExpireTime([]*ingress.Server)
@ -103,6 +104,14 @@ func (c *collector) ConfigSuccess(hash uint64, success bool) {
c.ingressController.ConfigSuccess(hash, success)
}
func (c *collector) IncCheckCount(namespace string, name string) {
c.ingressController.IncCheckCount(namespace, name)
}
func (c *collector) IncCheckErrorCount(namespace string, name string) {
c.ingressController.IncCheckErrorCount(namespace, name)
}
func (c *collector) IncReloadCount() {
c.ingressController.IncReloadCount()
}

View file

@ -31,15 +31,15 @@ import (
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/zakjan/cert-chain-resolver/certUtil"
"k8s.io/klog"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/ingress-nginx/internal/file"
"k8s.io/ingress-nginx/internal/ingress"
"k8s.io/ingress-nginx/internal/watch"
"k8s.io/klog"
)
var (
@ -360,6 +360,23 @@ func AddOrUpdateDHParam(name string, dh []byte, fs file.Filesystem) (string, err
// GetFakeSSLCert creates a Self Signed Certificate
// Based in the code https://golang.org/src/crypto/tls/generate_cert.go
func GetFakeSSLCert(fs file.Filesystem) *ingress.SSLCert {
cert, key := getFakeHostSSLCert("ingress.local")
sslCert, err := CreateSSLCert(cert, key)
if err != nil {
klog.Fatalf("unexpected error creating fake SSL Cert: %v", err)
}
err = StoreSSLCertOnDisk(fs, fakeCertificateName, sslCert)
if err != nil {
klog.Fatalf("unexpected error storing fake SSL Cert: %v", err)
}
return sslCert
}
func getFakeHostSSLCert(host string) ([]byte, []byte) {
var priv interface{}
var err error
@ -392,7 +409,7 @@ func GetFakeSSLCert(fs file.Filesystem) *ingress.SSLCert {
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"ingress.local"},
DNSNames: []string{host},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.(*rsa.PrivateKey).PublicKey, priv)
if err != nil {
@ -403,17 +420,7 @@ func GetFakeSSLCert(fs file.Filesystem) *ingress.SSLCert {
key := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey))})
sslCert, err := CreateSSLCert(cert, key)
if err != nil {
klog.Fatalf("unexpected error creating fake SSL Cert: %v", err)
}
err = StoreSSLCertOnDisk(fs, fakeCertificateName, sslCert)
if err != nil {
klog.Fatalf("unexpected error storing fake SSL Cert: %v", err)
}
return sslCert
return cert, key
}
// FullChainCert checks if a certificate file contains issues in the intermediate CA chain
@ -470,3 +477,64 @@ func IsValidHostname(hostname string, commonNames []string) bool {
return false
}
// TLSListener implements a dynamic certificate loader
type TLSListener struct {
certificatePath string
keyPath string
fs file.Filesystem
certificate *tls.Certificate
err error
lock sync.Mutex
}
// NewTLSListener watches changes to th certificate and key paths
// and reloads it whenever it changes
func NewTLSListener(certificate, key string) *TLSListener {
fs, err := file.NewLocalFS()
if err != nil {
panic(fmt.Sprintf("failed to instanciate certificate: %v", err))
}
l := TLSListener{
certificatePath: certificate,
keyPath: key,
fs: fs,
lock: sync.Mutex{},
}
l.load()
watch.NewFileWatcher(certificate, l.load)
watch.NewFileWatcher(key, l.load)
return &l
}
// GetCertificate implements the tls.Config.GetCertificate interface
func (tl *TLSListener) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
tl.lock.Lock()
defer tl.lock.Unlock()
return tl.certificate, tl.err
}
// TLSConfig instanciates a TLS configuration, always providing an up to date certificate
func (tl *TLSListener) TLSConfig() *tls.Config {
return &tls.Config{
GetCertificate: tl.GetCertificate,
}
}
func (tl *TLSListener) load() {
klog.Infof("loading tls certificate from certificate path %s and key path %s", tl.certificatePath, tl.keyPath)
certBytes, err := tl.fs.ReadFile(tl.certificatePath)
if err != nil {
tl.certificate = nil
tl.err = err
}
keyBytes, err := tl.fs.ReadFile(tl.keyPath)
if err != nil {
tl.certificate = nil
tl.err = err
}
cert, err := tls.X509KeyPair(certBytes, keyBytes)
tl.lock.Lock()
defer tl.lock.Unlock()
tl.certificate, tl.err = &cert, err
}

View file

@ -22,6 +22,7 @@ import (
"crypto/rand"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
@ -29,10 +30,15 @@ import (
"fmt"
"math"
"math/big"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
certutil "k8s.io/client-go/util/cert"
"k8s.io/kubernetes/pkg/util/filesystem"
"k8s.io/ingress-nginx/internal/file"
)
@ -366,3 +372,87 @@ func encodeCertPEM(cert *x509.Certificate) []byte {
}
return pem.EncodeToMemory(&block)
}
func fakeCertificate(t *testing.T, fs filesystem.Filesystem) []byte {
cert, key := getFakeHostSSLCert("localhost")
fd, err := fs.Create("/key.crt")
if err != nil {
t.Errorf("failed to write test key: %v", err)
}
fd.Write(cert)
fd, err = fs.Create("/key.key")
if err != nil {
t.Errorf("failed to write test key: %v", err)
}
fd.Write(key)
return cert
}
func dialTestServer(port string, rootCertificates ...[]byte) error {
roots := x509.NewCertPool()
for _, cert := range rootCertificates {
ok := roots.AppendCertsFromPEM(cert)
if !ok {
return fmt.Errorf("failed to add root certificate")
}
}
resp, err := tls.Dial("tcp", "localhost:"+port, &tls.Config{
RootCAs: roots,
})
if err != nil {
return err
}
if resp.Handshake() != nil {
return fmt.Errorf("TLS handshake should succeed: %v", err)
}
return nil
}
func TestTLSKeyReloader(t *testing.T) {
fs := filesystem.NewFakeFs()
cert := fakeCertificate(t, fs)
watcher := TLSListener{
certificatePath: "/key.crt",
keyPath: "/key.key",
fs: fs,
lock: sync.Mutex{},
}
watcher.load()
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
s.Config.TLSConfig = watcher.TLSConfig()
s.Listener = tls.NewListener(s.Listener, s.Config.TLSConfig)
go s.Start()
defer s.Close()
port := strings.Split(s.Listener.Addr().String(), ":")[1]
t.Run("without the trusted certificate", func(t *testing.T) {
if dialTestServer(port) == nil {
t.Errorf("TLS dial should fail")
}
})
t.Run("with the certificate trustes as root certificate", func(t *testing.T) {
if err := dialTestServer(port, cert); err != nil {
t.Errorf("TLS dial should succeed, got error: %v", err)
}
})
t.Run("with a new certificate", func(t *testing.T) {
newCert := fakeCertificate(t, fs)
t.Run("when the certificate is not reloaded", func(t *testing.T) {
if dialTestServer(port, newCert) == nil {
t.Errorf("TLS dial should fail")
}
})
// simulate watch.NewFileWatcher to call the load function
watcher.load()
t.Run("when the certificate is reloaded", func(t *testing.T) {
if err := dialTestServer(port, newCert); err != nil {
t.Errorf("TLS dial should succeed, got error: %v", err)
}
})
})
}

View file

@ -40,6 +40,7 @@ nav:
- Installation Guide: "deploy/index.md"
- Bare-metal considerations: "deploy/baremetal.md"
- Role Based Access Control (RBAC): "deploy/rbac.md"
- Validating Webhook (admission controller): "deploy/validating-webhook.md"
- Upgrade: "deploy/upgrade.md"
- User guide:
- NGINX Configuration:

23
vendor/k8s.io/api/admission/v1beta1/doc.go generated vendored Normal file
View file

@ -0,0 +1,23 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:deepcopy-gen=package
// +k8s:protobuf-gen=package
// +k8s:openapi-gen=false
// +groupName=admission.k8s.io
package v1beta1 // import "k8s.io/api/admission/v1beta1"

1390
vendor/k8s.io/api/admission/v1beta1/generated.pb.go generated vendored Normal file

File diff suppressed because it is too large Load diff

123
vendor/k8s.io/api/admission/v1beta1/generated.proto generated vendored Normal file
View file

@ -0,0 +1,123 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This file was autogenerated by go-to-protobuf. Do not edit it manually!
syntax = 'proto2';
package k8s.io.api.admission.v1beta1;
import "k8s.io/api/authentication/v1/generated.proto";
import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto";
// Package-wide variables from generator "generated".
option go_package = "v1beta1";
// AdmissionRequest describes the admission.Attributes for the admission request.
message AdmissionRequest {
// UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are
// otherwise identical (parallel requests, requests when earlier requests did not modify etc)
// The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request.
// It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.
optional string uid = 1;
// Kind is the type of object being manipulated. For example: Pod
optional k8s.io.apimachinery.pkg.apis.meta.v1.GroupVersionKind kind = 2;
// Resource is the name of the resource being requested. This is not the kind. For example: pods
optional k8s.io.apimachinery.pkg.apis.meta.v1.GroupVersionResource resource = 3;
// SubResource is the name of the subresource being requested. This is a different resource, scoped to the parent
// resource, but it may have a different kind. For instance, /pods has the resource "pods" and the kind "Pod", while
// /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod" (because status operates on
// pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource
// "binding", and kind "Binding".
// +optional
optional string subResource = 4;
// Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and
// rely on the server to generate the name. If that is the case, this method will return the empty string.
// +optional
optional string name = 5;
// Namespace is the namespace associated with the request (if any).
// +optional
optional string namespace = 6;
// Operation is the operation being performed
optional string operation = 7;
// UserInfo is information about the requesting user
optional k8s.io.api.authentication.v1.UserInfo userInfo = 8;
// Object is the object from the incoming request prior to default values being applied
// +optional
optional k8s.io.apimachinery.pkg.runtime.RawExtension object = 9;
// OldObject is the existing object. Only populated for UPDATE requests.
// +optional
optional k8s.io.apimachinery.pkg.runtime.RawExtension oldObject = 10;
// DryRun indicates that modifications will definitely not be persisted for this request.
// Defaults to false.
// +optional
optional bool dryRun = 11;
}
// AdmissionResponse describes an admission response.
message AdmissionResponse {
// UID is an identifier for the individual request/response.
// This should be copied over from the corresponding AdmissionRequest.
optional string uid = 1;
// Allowed indicates whether or not the admission request was permitted.
optional bool allowed = 2;
// Result contains extra details into why an admission request was denied.
// This field IS NOT consulted in any way if "Allowed" is "true".
// +optional
optional k8s.io.apimachinery.pkg.apis.meta.v1.Status status = 3;
// The patch body. Currently we only support "JSONPatch" which implements RFC 6902.
// +optional
optional bytes patch = 4;
// The type of Patch. Currently we only allow "JSONPatch".
// +optional
optional string patchType = 5;
// AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted).
// MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controller will prefix the keys with
// admission webhook name (e.g. imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will be provided by
// the admission webhook to add additional context to the audit log for this request.
// +optional
map<string, string> auditAnnotations = 6;
}
// AdmissionReview describes an admission review request/response.
message AdmissionReview {
// Request describes the attributes for the admission request.
// +optional
optional AdmissionRequest request = 1;
// Response describes the attributes for the admission response.
// +optional
optional AdmissionResponse response = 2;
}

51
vendor/k8s.io/api/admission/v1beta1/register.go generated vendored Normal file
View file

@ -0,0 +1,51 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name for this API.
const GroupName = "admission.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&AdmissionReview{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

127
vendor/k8s.io/api/admission/v1beta1/types.go generated vendored Normal file
View file

@ -0,0 +1,127 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
import (
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// AdmissionReview describes an admission review request/response.
type AdmissionReview struct {
metav1.TypeMeta `json:",inline"`
// Request describes the attributes for the admission request.
// +optional
Request *AdmissionRequest `json:"request,omitempty" protobuf:"bytes,1,opt,name=request"`
// Response describes the attributes for the admission response.
// +optional
Response *AdmissionResponse `json:"response,omitempty" protobuf:"bytes,2,opt,name=response"`
}
// AdmissionRequest describes the admission.Attributes for the admission request.
type AdmissionRequest struct {
// UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are
// otherwise identical (parallel requests, requests when earlier requests did not modify etc)
// The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request.
// It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.
UID types.UID `json:"uid" protobuf:"bytes,1,opt,name=uid"`
// Kind is the type of object being manipulated. For example: Pod
Kind metav1.GroupVersionKind `json:"kind" protobuf:"bytes,2,opt,name=kind"`
// Resource is the name of the resource being requested. This is not the kind. For example: pods
Resource metav1.GroupVersionResource `json:"resource" protobuf:"bytes,3,opt,name=resource"`
// SubResource is the name of the subresource being requested. This is a different resource, scoped to the parent
// resource, but it may have a different kind. For instance, /pods has the resource "pods" and the kind "Pod", while
// /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod" (because status operates on
// pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource
// "binding", and kind "Binding".
// +optional
SubResource string `json:"subResource,omitempty" protobuf:"bytes,4,opt,name=subResource"`
// Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and
// rely on the server to generate the name. If that is the case, this method will return the empty string.
// +optional
Name string `json:"name,omitempty" protobuf:"bytes,5,opt,name=name"`
// Namespace is the namespace associated with the request (if any).
// +optional
Namespace string `json:"namespace,omitempty" protobuf:"bytes,6,opt,name=namespace"`
// Operation is the operation being performed
Operation Operation `json:"operation" protobuf:"bytes,7,opt,name=operation"`
// UserInfo is information about the requesting user
UserInfo authenticationv1.UserInfo `json:"userInfo" protobuf:"bytes,8,opt,name=userInfo"`
// Object is the object from the incoming request prior to default values being applied
// +optional
Object runtime.RawExtension `json:"object,omitempty" protobuf:"bytes,9,opt,name=object"`
// OldObject is the existing object. Only populated for UPDATE requests.
// +optional
OldObject runtime.RawExtension `json:"oldObject,omitempty" protobuf:"bytes,10,opt,name=oldObject"`
// DryRun indicates that modifications will definitely not be persisted for this request.
// Defaults to false.
// +optional
DryRun *bool `json:"dryRun,omitempty" protobuf:"varint,11,opt,name=dryRun"`
}
// AdmissionResponse describes an admission response.
type AdmissionResponse struct {
// UID is an identifier for the individual request/response.
// This should be copied over from the corresponding AdmissionRequest.
UID types.UID `json:"uid" protobuf:"bytes,1,opt,name=uid"`
// Allowed indicates whether or not the admission request was permitted.
Allowed bool `json:"allowed" protobuf:"varint,2,opt,name=allowed"`
// Result contains extra details into why an admission request was denied.
// This field IS NOT consulted in any way if "Allowed" is "true".
// +optional
Result *metav1.Status `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
// The patch body. Currently we only support "JSONPatch" which implements RFC 6902.
// +optional
Patch []byte `json:"patch,omitempty" protobuf:"bytes,4,opt,name=patch"`
// The type of Patch. Currently we only allow "JSONPatch".
// +optional
PatchType *PatchType `json:"patchType,omitempty" protobuf:"bytes,5,opt,name=patchType"`
// AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted).
// MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controller will prefix the keys with
// admission webhook name (e.g. imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will be provided by
// the admission webhook to add additional context to the audit log for this request.
// +optional
AuditAnnotations map[string]string `json:"auditAnnotations,omitempty" protobuf:"bytes,6,opt,name=auditAnnotations"`
}
// PatchType is the type of patch being used to represent the mutated object
type PatchType string
// PatchType constants.
const (
PatchTypeJSONPatch PatchType = "JSONPatch"
)
// Operation is the type of resource operation being checked for admission control
type Operation string
// Operation constants
const (
Create Operation = "CREATE"
Update Operation = "UPDATE"
Delete Operation = "DELETE"
Connect Operation = "CONNECT"
)

View file

@ -0,0 +1,73 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1beta1
// This file contains a collection of methods that can be used from go-restful to
// generate Swagger API documentation for its models. Please read this PR for more
// information on the implementation: https://github.com/emicklei/go-restful/pull/215
//
// TODOs are ignored from the parser (e.g. TODO(andronat):... || TODO:...) if and only if
// they are on one line! For multiple line or blocks that you want to ignore use ---.
// Any context after a --- is ignored.
//
// Those methods can be generated by using hack/update-generated-swagger-docs.sh
// AUTO-GENERATED FUNCTIONS START HERE. DO NOT EDIT.
var map_AdmissionRequest = map[string]string{
"": "AdmissionRequest describes the admission.Attributes for the admission request.",
"uid": "UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are otherwise identical (parallel requests, requests when earlier requests did not modify etc) The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request. It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.",
"kind": "Kind is the type of object being manipulated. For example: Pod",
"resource": "Resource is the name of the resource being requested. This is not the kind. For example: pods",
"subResource": "SubResource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind. For instance, /pods has the resource \"pods\" and the kind \"Pod\", while /pods/foo/status has the resource \"pods\", the sub resource \"status\", and the kind \"Pod\" (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource \"pods\", subresource \"binding\", and kind \"Binding\".",
"name": "Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and rely on the server to generate the name. If that is the case, this method will return the empty string.",
"namespace": "Namespace is the namespace associated with the request (if any).",
"operation": "Operation is the operation being performed",
"userInfo": "UserInfo is information about the requesting user",
"object": "Object is the object from the incoming request prior to default values being applied",
"oldObject": "OldObject is the existing object. Only populated for UPDATE requests.",
"dryRun": "DryRun indicates that modifications will definitely not be persisted for this request. Defaults to false.",
}
func (AdmissionRequest) SwaggerDoc() map[string]string {
return map_AdmissionRequest
}
var map_AdmissionResponse = map[string]string{
"": "AdmissionResponse describes an admission response.",
"uid": "UID is an identifier for the individual request/response. This should be copied over from the corresponding AdmissionRequest.",
"allowed": "Allowed indicates whether or not the admission request was permitted.",
"status": "Result contains extra details into why an admission request was denied. This field IS NOT consulted in any way if \"Allowed\" is \"true\".",
"patch": "The patch body. Currently we only support \"JSONPatch\" which implements RFC 6902.",
"patchType": "The type of Patch. Currently we only allow \"JSONPatch\".",
"auditAnnotations": "AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted). MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controller will prefix the keys with admission webhook name (e.g. imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will be provided by the admission webhook to add additional context to the audit log for this request.",
}
func (AdmissionResponse) SwaggerDoc() map[string]string {
return map_AdmissionResponse
}
var map_AdmissionReview = map[string]string{
"": "AdmissionReview describes an admission review request/response.",
"request": "Request describes the attributes for the admission request.",
"response": "Response describes the attributes for the admission response.",
}
func (AdmissionReview) SwaggerDoc() map[string]string {
return map_AdmissionReview
}
// AUTO-GENERATED FUNCTIONS END HERE

View file

@ -0,0 +1,125 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1beta1
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmissionRequest) DeepCopyInto(out *AdmissionRequest) {
*out = *in
out.Kind = in.Kind
out.Resource = in.Resource
in.UserInfo.DeepCopyInto(&out.UserInfo)
in.Object.DeepCopyInto(&out.Object)
in.OldObject.DeepCopyInto(&out.OldObject)
if in.DryRun != nil {
in, out := &in.DryRun, &out.DryRun
*out = new(bool)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionRequest.
func (in *AdmissionRequest) DeepCopy() *AdmissionRequest {
if in == nil {
return nil
}
out := new(AdmissionRequest)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmissionResponse) DeepCopyInto(out *AdmissionResponse) {
*out = *in
if in.Result != nil {
in, out := &in.Result, &out.Result
*out = new(v1.Status)
(*in).DeepCopyInto(*out)
}
if in.Patch != nil {
in, out := &in.Patch, &out.Patch
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.PatchType != nil {
in, out := &in.PatchType, &out.PatchType
*out = new(PatchType)
**out = **in
}
if in.AuditAnnotations != nil {
in, out := &in.AuditAnnotations, &out.AuditAnnotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionResponse.
func (in *AdmissionResponse) DeepCopy() *AdmissionResponse {
if in == nil {
return nil
}
out := new(AdmissionResponse)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmissionReview) DeepCopyInto(out *AdmissionReview) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Request != nil {
in, out := &in.Request, &out.Request
*out = new(AdmissionRequest)
(*in).DeepCopyInto(*out)
}
if in.Response != nil {
in, out := &in.Response, &out.Response
*out = new(AdmissionResponse)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionReview.
func (in *AdmissionReview) DeepCopy() *AdmissionReview {
if in == nil {
return nil
}
out := new(AdmissionReview)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AdmissionReview) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}