mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Example webhook implementation (used in E2E test)
This commit is contained in:
parent
c3d05c816d
commit
8235e389fb
4
test/images/crd-conversion-webhook/BASEIMAGE
Normal file
4
test/images/crd-conversion-webhook/BASEIMAGE
Normal file
@ -0,0 +1,4 @@
|
||||
amd64=alpine:3.6
|
||||
arm=arm32v6/alpine:3.6
|
||||
arm64=arm64v8/alpine:3.6
|
||||
ppc64le=ppc64le/alpine:3.6
|
18
test/images/crd-conversion-webhook/Dockerfile
Normal file
18
test/images/crd-conversion-webhook/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright 2018 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.
|
||||
|
||||
FROM BASEIMAGE
|
||||
|
||||
ADD crd_conversion_webhook /crd_conversion_webhook
|
||||
ENTRYPOINT ["/crd_conversion_webhook"]
|
25
test/images/crd-conversion-webhook/Makefile
Normal file
25
test/images/crd-conversion-webhook/Makefile
Normal file
@ -0,0 +1,25 @@
|
||||
# Copyright 2018 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.
|
||||
|
||||
SRCS=crd_conversion_webhook
|
||||
ARCH ?= amd64
|
||||
TARGET ?= $(CURDIR)
|
||||
GOLANG_VERSION ?= latest
|
||||
SRC_DIR = $(notdir $(shell pwd))
|
||||
export
|
||||
|
||||
bin:
|
||||
../image-util.sh bin $(SRCS)
|
||||
|
||||
.PHONY: bin
|
11
test/images/crd-conversion-webhook/README.md
Normal file
11
test/images/crd-conversion-webhook/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Kubernetes External Admission Webhook Test Image
|
||||
|
||||
The image tests CustomResourceConversionWebhook. After deploying it to kubernetes cluster,
|
||||
administrator needs to create a CustomResourceConversion.Webhook
|
||||
in kubernetes cluster to use remote webhook for conversions.
|
||||
|
||||
## Build the code
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
1
test/images/crd-conversion-webhook/VERSION
Normal file
1
test/images/crd-conversion-webhook/VERSION
Normal file
@ -0,0 +1 @@
|
||||
1.13rev2
|
51
test/images/crd-conversion-webhook/config.go
Normal file
51
test/images/crd-conversion-webhook/config.go
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2018 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 main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Get a clientset with in-cluster config.
|
||||
func getClient() *kubernetes.Clientset {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
return clientset
|
||||
}
|
||||
|
||||
func configTLS(config Config, clientset *kubernetes.Clientset) *tls.Config {
|
||||
sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{sCert},
|
||||
// TODO: uses mutual tls after we agree on what cert the apiserver should use.
|
||||
// ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2018 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 converter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
)
|
||||
|
||||
func TestConverter(t *testing.T) {
|
||||
sampleObj := `kind: ConversionReview
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
request:
|
||||
uid: 0000-0000-0000-0000
|
||||
desiredAPIVersion: stable.example.com/v2
|
||||
objects:
|
||||
- apiVersion: stable.example.com/v1
|
||||
kind: CronTab
|
||||
metadata:
|
||||
name: my-new-cron-object
|
||||
spec:
|
||||
cronSpec: "* * * * */5"
|
||||
image: my-awesome-cron-image
|
||||
hostPort: "localhost:7070"
|
||||
`
|
||||
// First try json, it should fail as the data is taml
|
||||
response := httptest.NewRecorder()
|
||||
request, err := http.NewRequest("POST", "/convert", strings.NewReader(sampleObj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
ServeExampleConvert(response, request)
|
||||
convertReview := v1beta1.ConversionReview{}
|
||||
scheme := runtime.NewScheme()
|
||||
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
||||
if _, _, err := jsonSerializer.Decode(response.Body.Bytes(), nil, &convertReview); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if convertReview.Response.Result.Status != v1.StatusFailure {
|
||||
t.Fatalf("expected the operation to fail when yaml is provided with json header")
|
||||
} else if !strings.Contains(convertReview.Response.Result.Message, "json parse error") {
|
||||
t.Fatalf("expected to fail on json parser, but it failed with: %v", convertReview.Response.Result.Message)
|
||||
}
|
||||
|
||||
// Now try yaml, and it should successfully convert
|
||||
response = httptest.NewRecorder()
|
||||
request, err = http.NewRequest("POST", "/convert", strings.NewReader(sampleObj))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request.Header.Add("Content-Type", "application/yaml")
|
||||
ServeExampleConvert(response, request)
|
||||
convertReview = v1beta1.ConversionReview{}
|
||||
yamlSerializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme)
|
||||
if _, _, err := yamlSerializer.Decode(response.Body.Bytes(), nil, &convertReview); err != nil {
|
||||
t.Fatalf("cannot decode data: \n %v\n Error: %v", response.Body, err)
|
||||
}
|
||||
if convertReview.Response.Result.Status != v1.StatusSuccess {
|
||||
t.Fatalf("cr conversion failed: %v", convertReview.Response)
|
||||
}
|
||||
convertedObj := unstructured.Unstructured{}
|
||||
if _, _, err := yamlSerializer.Decode(convertReview.Response.ConvertedObjects[0].Raw, nil, &convertedObj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, a := "stable.example.com/v2", convertedObj.GetAPIVersion(); e != a {
|
||||
t.Errorf("expected= %v, actual= %v", e, a)
|
||||
}
|
||||
if e, a := "localhost", convertedObj.Object["host"]; e != a {
|
||||
t.Errorf("expected= %v, actual= %v", e, a)
|
||||
}
|
||||
if e, a := "7070", convertedObj.Object["port"]; e != a {
|
||||
t.Errorf("expected= %v, actual= %v", e, a)
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2018 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 converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func convertExampleCRD(Object *unstructured.Unstructured, toVersion string) (*unstructured.Unstructured, metav1.Status) {
|
||||
glog.V(2).Info("converting crd")
|
||||
|
||||
convertedObject := Object.DeepCopy()
|
||||
fromVersion := Object.GetAPIVersion()
|
||||
|
||||
if toVersion == fromVersion {
|
||||
return nil, statusErrorWithMessage("conversion from a version to itself should not call the webhook: %s", toVersion)
|
||||
}
|
||||
|
||||
switch Object.GetAPIVersion() {
|
||||
case "stable.example.com/v1":
|
||||
switch toVersion {
|
||||
case "stable.example.com/v2":
|
||||
hostPort, ok := convertedObject.Object["hostPort"]
|
||||
if ok {
|
||||
delete(convertedObject.Object, "hostPort")
|
||||
parts := strings.Split(hostPort.(string), ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, statusErrorWithMessage("invalid hostPort value `%v`", hostPort)
|
||||
}
|
||||
convertedObject.Object["host"] = parts[0]
|
||||
convertedObject.Object["port"] = parts[1]
|
||||
}
|
||||
default:
|
||||
return nil, statusErrorWithMessage("unexpected conversion version %q", toVersion)
|
||||
}
|
||||
case "stable.example.com/v2":
|
||||
switch toVersion {
|
||||
case "stable.example.com/v1":
|
||||
host, hasHost := convertedObject.Object["host"]
|
||||
port, hasPort := convertedObject.Object["port"]
|
||||
if hasHost || hasPort {
|
||||
if !hasHost {
|
||||
host = ""
|
||||
}
|
||||
if !hasPort {
|
||||
port = ""
|
||||
}
|
||||
convertedObject.Object["hostPort"] = fmt.Sprintf("%s:%s", host, port)
|
||||
delete(convertedObject.Object, "host")
|
||||
delete(convertedObject.Object, "port")
|
||||
}
|
||||
default:
|
||||
return nil, statusErrorWithMessage("unexpected conversion version %q", toVersion)
|
||||
}
|
||||
default:
|
||||
return nil, statusErrorWithMessage("unexpected conversion version %q", fromVersion)
|
||||
}
|
||||
return convertedObject, statusSucceed()
|
||||
}
|
178
test/images/crd-conversion-webhook/converter/framework.go
Normal file
178
test/images/crd-conversion-webhook/converter/framework.go
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
Copyright 2018 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 converter
|
||||
|
||||
import (
|
||||
"bitbucket.org/ww/goautoneg"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
)
|
||||
|
||||
// convertFunc is the user defined function for any conversion. The code in this file is a
|
||||
// template that can be use for any CR conversion given this function.
|
||||
type convertFunc func(Object *unstructured.Unstructured, version string) (*unstructured.Unstructured, metav1.Status)
|
||||
|
||||
// conversionResponseFailureWithMessagef is a helper function to create an AdmissionResponse
|
||||
// with a formatted embedded error message.
|
||||
func conversionResponseFailureWithMessagef(msg string, params ...interface{}) *v1beta1.ConversionResponse {
|
||||
return &v1beta1.ConversionResponse{
|
||||
Result: metav1.Status{
|
||||
Message: fmt.Sprintf(msg, params...),
|
||||
Status: metav1.StatusFailure,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func statusErrorWithMessage(msg string, params ...interface{}) metav1.Status {
|
||||
return metav1.Status{
|
||||
Message: fmt.Sprintf(msg, params...),
|
||||
Status: metav1.StatusFailure,
|
||||
}
|
||||
}
|
||||
|
||||
func statusSucceed() metav1.Status {
|
||||
return metav1.Status{
|
||||
Status: metav1.StatusSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
// doConversion converts the requested object given the conversion function and returns a conversion response.
|
||||
// failures will be reported as Reason in the conversion response.
|
||||
func doConversion(convertRequest *v1beta1.ConversionRequest, convert convertFunc) *v1beta1.ConversionResponse {
|
||||
var convertedObjects []runtime.RawExtension
|
||||
for _, obj := range convertRequest.Objects {
|
||||
cr := unstructured.Unstructured{}
|
||||
if err := cr.UnmarshalJSON(obj.Raw); err != nil {
|
||||
glog.Error(err)
|
||||
return conversionResponseFailureWithMessagef("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err)
|
||||
}
|
||||
convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion)
|
||||
if status.Status != metav1.StatusSuccess {
|
||||
glog.Error(status.String())
|
||||
return &v1beta1.ConversionResponse{
|
||||
Result: status,
|
||||
}
|
||||
}
|
||||
convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion)
|
||||
convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR})
|
||||
}
|
||||
return &v1beta1.ConversionResponse{
|
||||
ConvertedObjects: convertedObjects,
|
||||
Result: statusSucceed(),
|
||||
}
|
||||
}
|
||||
|
||||
func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) {
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
if data, err := ioutil.ReadAll(r.Body); err == nil {
|
||||
body = data
|
||||
}
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
serializer := getInputSerializer(contentType)
|
||||
if serializer == nil {
|
||||
msg := fmt.Sprintf("invalid Content-Type header `%s`", contentType)
|
||||
glog.Errorf(msg)
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(2).Infof("handling request: %v", body)
|
||||
convertReview := v1beta1.ConversionReview{}
|
||||
if _, _, err := serializer.Decode(body, nil, &convertReview); err != nil {
|
||||
glog.Error(err)
|
||||
convertReview.Response = conversionResponseFailureWithMessagef("failed to deserialize body (%v) with error %v", string(body), err)
|
||||
} else {
|
||||
convertReview.Response = doConversion(convertReview.Request, convert)
|
||||
convertReview.Response.UID = convertReview.Request.UID
|
||||
}
|
||||
glog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response))
|
||||
|
||||
// reset the request, it is not needed in a response.
|
||||
convertReview.Request = &v1beta1.ConversionRequest{}
|
||||
|
||||
accept := r.Header.Get("Accept")
|
||||
outSerializer := getOutputSerializer(accept)
|
||||
if outSerializer == nil {
|
||||
msg := fmt.Sprintf("invalid accept header `%s`", accept)
|
||||
glog.Errorf(msg)
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := outSerializer.Encode(&convertReview, w)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ServeExampleConvert servers endpoint for the example converter defined as convertExampleCRD function.
|
||||
func ServeExampleConvert(w http.ResponseWriter, r *http.Request) {
|
||||
serve(w, r, convertExampleCRD)
|
||||
}
|
||||
|
||||
type mediaType struct {
|
||||
Type, SubType string
|
||||
}
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
var serializers = map[mediaType]runtime.Serializer{
|
||||
{"application", "json"}: json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false),
|
||||
{"application", "yaml"}: json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme),
|
||||
}
|
||||
|
||||
func getInputSerializer(contentType string) runtime.Serializer {
|
||||
parts := strings.SplitN(contentType, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil
|
||||
}
|
||||
return serializers[mediaType{parts[0], parts[1]}]
|
||||
}
|
||||
|
||||
func getOutputSerializer(accept string) runtime.Serializer {
|
||||
if len(accept) == 0 {
|
||||
return serializers[mediaType{"application", "json"}]
|
||||
}
|
||||
|
||||
clauses := goautoneg.ParseAccept(accept)
|
||||
for _, clause := range clauses {
|
||||
for k, v := range serializers {
|
||||
switch {
|
||||
case clause.Type == k.Type && clause.SubType == k.SubType,
|
||||
clause.Type == k.Type && clause.SubType == "*",
|
||||
clause.Type == "*" && clause.SubType == "*":
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
52
test/images/crd-conversion-webhook/main.go
Normal file
52
test/images/crd-conversion-webhook/main.go
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2018 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/kubernetes/test/images/crd-conversion-webhook/converter"
|
||||
)
|
||||
|
||||
// Config contains the server (the webhook) cert and key.
|
||||
type Config struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func (c *Config) addFlags() {
|
||||
flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+
|
||||
"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+
|
||||
"after server cert).")
|
||||
flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+
|
||||
"File containing the default x509 private key matching --tls-cert-file.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
var config Config
|
||||
config.addFlags()
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/crdconvert", converter.ServeExampleConvert)
|
||||
clientset := getClient()
|
||||
server := &http.Server{
|
||||
Addr: ":443",
|
||||
TLSConfig: configTLS(config, clientset),
|
||||
}
|
||||
server.ListenAndServeTLS("", "")
|
||||
}
|
Loading…
Reference in New Issue
Block a user