blob: e0b327f84343639764df876a41ae81eb51dc71e6 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 webhook_test
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
webhook2 "github.com/apache/dubbo-kubernetes/pkg/config/webhook"
dubbo_cp "github.com/apache/dubbo-kubernetes/pkg/config/app/dubbo-cp"
"github.com/apache/dubbo-kubernetes/pkg/core/cert/provider"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
admissionV1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
"github.com/apache/dubbo-kubernetes/pkg/webhook/webhook"
)
func TestServe(t *testing.T) {
t.Parallel()
authority := provider.GenerateAuthorityCert(nil, 60*60*1000)
c := provider.SignServerCert(authority, []string{"localhost"}, 60*60*1000)
server := webhook.NewWebhook(func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return c.GetTlsCert(), nil
})
port := getAvailablePort()
server.Init(&dubbo_cp.Config{
Webhook: webhook2.Webhook{
Port: port,
},
})
go server.Serve()
assert.Eventually(t, func() bool {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(authority.CertPem))
trans := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
}
client := http.Client{Transport: trans, Timeout: 15 * time.Second}
res, err := client.Get("https://localhost:" + strconv.Itoa(int(port)) + "/health")
if err != nil {
t.Log("cp-server is not ready: ", err)
return false
}
if res.StatusCode != http.StatusOK {
t.Fatal("unexpected status code: ", res.StatusCode)
return false
}
return true
}, 30*time.Second, 1*time.Second, "cp-server should be ready")
server.Stop()
}
func getAvailablePort() int32 {
address, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:0")
listener, _ := net.ListenTCP("tcp", address)
defer listener.Close()
return int32(listener.Addr().(*net.TCPAddr).Port)
}
func TestMutate_MediaError1(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
request, err := http.NewRequest("POST", "/mutating-services", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
}
func TestMutate_MediaError2(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
request, err := http.NewRequest("POST", "/mutating-services", nil)
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/xml")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
}
func TestMutate_BodyError(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
data := "{"
request, err := http.NewRequest("POST", "/mutating-services", strings.NewReader(data))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusOK, w.Code)
body, err := io.ReadAll(w.Body)
assert.Nil(t, err)
expected, err := json.Marshal(admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionV1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Failure",
Message: "unexpected end of JSON input",
Reason: metav1.StatusReason("unexpected end of JSON input"),
},
},
})
assert.Equal(t, string(expected), string(body))
assert.Nil(t, err)
}
func TestMutate_AdmitEmpty(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
data, err := json.Marshal(admissionV1.AdmissionReview{})
assert.Nil(t, err)
request, err := http.NewRequest("POST", "/mutating-services", strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusOK, w.Code)
body, err := io.ReadAll(w.Body)
assert.Nil(t, err)
expected, err := json.Marshal(admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionV1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Failure",
Message: "[Webhook] AdmissionReview request is nil",
Reason: "[Webhook] AdmissionReview request is nil",
},
},
})
assert.Equal(t, string(expected), string(body))
assert.Nil(t, err)
}
func TestMutate_AdmitErrorType(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
data, err := json.Marshal(admissionV1.AdmissionReview{
Request: &admissionV1.AdmissionRequest{
UID: "123",
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "deployments"},
},
})
assert.Nil(t, err)
request, err := http.NewRequest("POST", "/mutating-services", strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusOK, w.Code)
body, err := io.ReadAll(w.Body)
assert.Nil(t, err)
expected, err := json.Marshal(admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionV1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Failure",
Message: "[Webhook] expect resource to be pods, but actual is { v1 deployments}",
Reason: "[Webhook] expect resource to be pods, but actual is { v1 deployments}",
},
},
})
assert.Equal(t, string(expected), string(body))
assert.Nil(t, err)
}
func TestMutate_AdmitPodPatchErr(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
server.Patches = []webhook.PodPatch{
func(pod *v1.Pod) (*v1.Pod, error) {
if pod.Name == "" {
return nil, fmt.Errorf("Name is empty")
}
pod.Name = "Target"
return pod, nil
},
}
data, err := json.Marshal(admissionV1.AdmissionReview{
Request: &admissionV1.AdmissionRequest{
UID: "123",
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: runtime.RawExtension{
Raw: []byte("{}"),
},
},
})
assert.Nil(t, err)
request, err := http.NewRequest("POST", "/mutating-services", strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusOK, w.Code)
body, err := io.ReadAll(w.Body)
assert.Nil(t, err)
expected, err := json.Marshal(admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionV1.AdmissionResponse{
Allowed: true,
Result: &metav1.Status{
Status: "Failure",
Message: "[Webhook] Patch error: . Msg: [Webhook] Pod patch failed: Name is empty",
Reason: "[Webhook] Patch error: . Msg: [Webhook] Pod patch failed: Name is empty",
},
},
})
assert.Equal(t, string(expected), string(body))
assert.Nil(t, err)
}
func TestMutate_AdmitPodPatch(t *testing.T) {
t.Parallel()
server := webhook.NewWebhook(nil)
server.Patches = []webhook.PodPatch{
func(pod *v1.Pod) (*v1.Pod, error) {
if pod.Name == "" {
return nil, fmt.Errorf("Name is empty")
}
pod.Name = "Target"
return pod, nil
},
}
originPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
origin, err := json.Marshal(originPod)
assert.Nil(t, err)
data, err := json.Marshal(admissionV1.AdmissionReview{
Request: &admissionV1.AdmissionRequest{
UID: "123",
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: runtime.RawExtension{
Raw: origin,
},
},
})
assert.Nil(t, err)
request, err := http.NewRequest("POST", "/mutating-services", strings.NewReader(string(data)))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
server.Mutate(w, request)
assert.Equal(t, http.StatusOK, w.Code)
body, err := io.ReadAll(w.Body)
assert.Nil(t, err)
patchType := admissionV1.PatchTypeJSONPatch
expected, err := json.Marshal(admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: &admissionV1.AdmissionResponse{
UID: "123",
Allowed: true,
Patch: []byte("[{\"op\":\"replace\",\"path\":\"/metadata/name\",\"value\":\"Target\"}]"),
PatchType: &patchType,
},
})
assert.Equal(t, string(expected), string(body))
assert.Nil(t, err)
}