389 lines
13 KiB
Go
389 lines
13 KiB
Go
/*
|
|
Copyright 2021 The cert-manager 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 cmapichecker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/rest"
|
|
)
|
|
|
|
const (
|
|
crNoMutation = `{
|
|
"kind":"CertificateRequest",
|
|
"apiVersion":"cert-manager.io/v1",
|
|
"metadata":{
|
|
"name":"cmapichecker-0001",
|
|
"namespace":"test-namespace"
|
|
},
|
|
"spec":{
|
|
"issuerRef":{"name":"cmapichecker"},
|
|
"request":"PENTUi1WQUxVRT4="
|
|
}
|
|
}`
|
|
crAfterMutation = `{
|
|
"kind":"CertificateRequest",
|
|
"apiVersion":"cert-manager.io/v1",
|
|
"metadata":{
|
|
"name":"cmapichecker-0001",
|
|
"namespace":"test-namespace"
|
|
},
|
|
"spec":{
|
|
"issuerRef":{"name":"cmapichecker"},
|
|
"request":"PENTUi1WQUxVRT4=",
|
|
"username":"test-user",
|
|
"uid":"test-uid"
|
|
},
|
|
"status":{}
|
|
}`
|
|
)
|
|
|
|
func TestCheck(t *testing.T) {
|
|
type testT struct {
|
|
apisResponse func(t *testing.T, r *http.Request) (int, []byte)
|
|
discoveryResponse func(t *testing.T, r *http.Request) (int, []byte)
|
|
createValidResponse func(t *testing.T, r *http.Request) (int, []byte)
|
|
createInvalidResponse func(t *testing.T, r *http.Request) (int, []byte)
|
|
|
|
expectedError string
|
|
expectedSimpleError string
|
|
}
|
|
|
|
tests := map[string]testT{
|
|
"no errors": {},
|
|
"without any cert-manager CRDs installed (missing from /apis)": {
|
|
apisResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusOK, []byte(`{
|
|
"kind": "APIGroupList",
|
|
"apiVersion": "v1",
|
|
"groups": []
|
|
}`)
|
|
},
|
|
expectedError: `error finding the scope of the object: failed to get restmapping: no matches for kind "CertificateRequest" in group "cert-manager.io"`,
|
|
expectedSimpleError: ErrCertManagerCRDsNotFound.Error(),
|
|
},
|
|
"without any cert-manager CRDs installed (404)": {
|
|
discoveryResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusNotFound, nil
|
|
},
|
|
expectedError: `error finding the scope of the object: failed to get restmapping: no matches for kind "CertificateRequest" in group "cert-manager.io"`,
|
|
expectedSimpleError: ErrCertManagerCRDsNotFound.Error(),
|
|
},
|
|
"without any cert-manager CRDs installed (empty list)": {
|
|
discoveryResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusOK, []byte(`{
|
|
"kind":"APIResourceList",
|
|
"apiVersion":"v1",
|
|
"groupVersion":"cert-manager.io/v1",
|
|
"resources":[]
|
|
}`)
|
|
},
|
|
expectedError: `error finding the scope of the object: failed to get restmapping: no matches for kind "CertificateRequest" in group "cert-manager.io"`,
|
|
expectedSimpleError: ErrCertManagerCRDsNotFound.Error(),
|
|
},
|
|
"without certificate request CRD installed": {
|
|
discoveryResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusOK, []byte(`{
|
|
"kind":"APIResourceList",
|
|
"apiVersion":"v1",
|
|
"groupVersion":"cert-manager.io/v1",
|
|
"resources":[
|
|
{
|
|
"name":"test",
|
|
"singularName":"",
|
|
"namespaced":true,
|
|
"kind":"Test",
|
|
"verbs":["get","patch","update"]
|
|
}
|
|
]
|
|
}`)
|
|
},
|
|
expectedError: `error finding the scope of the object: failed to get restmapping: no matches for kind "CertificateRequest" in group "cert-manager.io"`,
|
|
expectedSimpleError: ErrCertManagerCRDsNotFound.Error(),
|
|
},
|
|
"with missing certificate request endpoint": {
|
|
discoveryResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusNotFound, nil
|
|
},
|
|
expectedError: `error finding the scope of the object: failed to get restmapping: no matches for kind "CertificateRequest" in group "cert-manager.io"`,
|
|
expectedSimpleError: ErrCertManagerCRDsNotFound.Error(),
|
|
},
|
|
"dry-run certificate request was not mutated": {
|
|
createValidResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusOK, []byte(crNoMutation)
|
|
},
|
|
expectedError: ErrMutationWebhookMissing.Error(),
|
|
},
|
|
"cr was denied by 3rd party webhook": {
|
|
createInvalidResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusNotAcceptable, []byte(`{
|
|
"kind":"Status",
|
|
"apiVersion":"v1",
|
|
"metadata":{},
|
|
"status":"Failure",
|
|
"message":"admission webhook \"other-webhook.io\" denied the request: [ERROR MESSAGE]",
|
|
"reason":"NotAcceptable",
|
|
"code":406
|
|
}`)
|
|
},
|
|
expectedError: "admission webhook \"other-webhook.io\" denied the request: [ERROR MESSAGE]",
|
|
expectedSimpleError: ErrFailedToCheckAPI.Error(),
|
|
},
|
|
"missing validation error": {
|
|
createInvalidResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
return http.StatusOK, []byte(crAfterMutation)
|
|
},
|
|
expectedError: ErrValidatingWebhookMissing.Error(),
|
|
},
|
|
}
|
|
|
|
type testFailure struct {
|
|
message string
|
|
reason string
|
|
code int
|
|
simpleError string
|
|
}
|
|
|
|
for name, test := range map[string]testFailure{
|
|
"no permission": {
|
|
message: `certificaterequests.cert-manager.io is forbidden: User "test" cannot create resource "certificaterequests" in API group "cert-manager.io" in the namespace "test-namespace"`,
|
|
reason: "Forbidden",
|
|
code: http.StatusForbidden,
|
|
|
|
simpleError: ErrFailedToCheckAPI.Error(),
|
|
},
|
|
|
|
"service not found": {
|
|
message: `failed calling webhook "webhook.cert-manager.io": Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=10s": service "cert-manager-webhook" not found`,
|
|
reason: "InternalError",
|
|
code: 500,
|
|
|
|
simpleError: ErrWebhookServiceFailure.Error(),
|
|
},
|
|
"connection refused": {
|
|
message: `failed calling webhook "webhook.cert-manager.io": failed to call webhook: Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=30s": dial tcp 10.96.19.42:443: connect: connection refused`,
|
|
reason: "InternalError",
|
|
code: 500,
|
|
|
|
simpleError: ErrWebhookDeploymentFailure.Error(),
|
|
},
|
|
|
|
"certificate signed by unknown authority": {
|
|
message: `failed calling webhook "webhook.cert-manager.io": failed to call webhook: Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=30s": x509: certificate signed by unknown authority`,
|
|
reason: "NotAcceptable",
|
|
code: 406,
|
|
|
|
simpleError: ErrWebhookCertificateFailure.Error(),
|
|
},
|
|
"certificate signed by unknown authority (ECDSA verification failure)": {
|
|
message: `failed calling webhook "webhook.cert-manager.io": Post "https://cert-manager-webhook.cert-manager.svc:443/mutate?timeout=10s": x509: certificate signed by unknown authority (possibly because of "x509: ECDSA verification failure" while trying to verify candidate authority certificate "cert-manager-webhook-ca"`,
|
|
reason: "NotAcceptable",
|
|
code: 406,
|
|
|
|
simpleError: ErrWebhookCertificateFailure.Error(),
|
|
},
|
|
|
|
"validating webhook error (3rd party)": {
|
|
message: `admission webhook "other-webhook.io" denied the request: [ERROR MESSAGE]`,
|
|
reason: "NotAcceptable",
|
|
code: 406,
|
|
|
|
simpleError: ErrFailedToCheckAPI.Error(),
|
|
},
|
|
"missing mutating webhook": {
|
|
message: `admission webhook "webhook.cert-manager.io" denied the request: [spec.username: Forbidden: username identity must be that of the requester, spec.groups: Forbidden: groups identity must be that of the requester]`,
|
|
reason: "NotAcceptable",
|
|
code: 406,
|
|
|
|
simpleError: ErrMutationWebhookIncorrect.Error(),
|
|
},
|
|
"validating webhook error": {
|
|
message: `admission webhook "webhook.cert-manager.io" denied the request: spec.request: Invalid value: []byte{0x00}: error decoding certificate request PEM block`,
|
|
reason: "NotAcceptable",
|
|
code: 406,
|
|
|
|
simpleError: ErrMutationWebhookIncorrect.Error(),
|
|
},
|
|
|
|
"unknown error": {
|
|
message: `UNKNOWN ERROR`,
|
|
reason: "InternalError",
|
|
code: 500,
|
|
},
|
|
} {
|
|
tests["valid_failure_"+name] = testT{
|
|
createValidResponse: func(t *testing.T, r *http.Request) (int, []byte) {
|
|
byteResponse, err := json.Marshal(map[string]interface{}{
|
|
"kind": "Status",
|
|
"apiVersion": "v1",
|
|
"metadata": map[string]interface{}{},
|
|
"status": "Failure",
|
|
"message": test.message,
|
|
"reason": test.reason,
|
|
"code": test.code,
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
return test.code, byteResponse
|
|
},
|
|
expectedError: test.message,
|
|
expectedSimpleError: test.simpleError,
|
|
}
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
// fake https server to simulate the Kubernetes API server responses
|
|
mockKubernetesAPI := func(t *testing.T, r *http.Request) (int, []byte) {
|
|
switch r.URL.Path {
|
|
case "/api":
|
|
return http.StatusOK, []byte(`{"kind":"APIVersions","versions":["v1"]}`)
|
|
case "/apis":
|
|
if test.apisResponse != nil {
|
|
return test.apisResponse(t, r)
|
|
}
|
|
|
|
return http.StatusOK, []byte(`{
|
|
"kind": "APIGroupList",
|
|
"apiVersion": "v1",
|
|
"groups": [{
|
|
"name": "cert-manager.io",
|
|
"versions": [{
|
|
"groupVersion": "cert-manager.io/v1",
|
|
"version": "v1"
|
|
}],
|
|
"preferredVersion": {
|
|
"groupVersion": "cert-manager.io/v1",
|
|
"version": "v1"
|
|
}
|
|
}]
|
|
}`)
|
|
case "/apis/cert-manager.io/v1":
|
|
if test.discoveryResponse != nil {
|
|
return test.discoveryResponse(t, r)
|
|
}
|
|
|
|
return http.StatusOK, []byte(`{
|
|
"kind":"APIResourceList",
|
|
"apiVersion":"v1",
|
|
"groupVersion":"cert-manager.io/v1",
|
|
"resources":[
|
|
{
|
|
"name":"certificaterequests",
|
|
"singularName":"certificaterequest",
|
|
"namespaced":true,
|
|
"kind":"CertificateRequest",
|
|
"verbs":["delete","deletecollection","get","list","patch","create","update","watch"],
|
|
"shortNames":["cr","crs"],
|
|
"categories":["cert-manager"],
|
|
"storageVersionHash":"tuxiikMaACg="
|
|
},
|
|
{
|
|
"name":"certificaterequests/status",
|
|
"singularName":"",
|
|
"namespaced":true,
|
|
"kind":"CertificateRequest",
|
|
"verbs":["get","patch","update"]
|
|
}
|
|
]
|
|
}`)
|
|
case "/apis/cert-manager.io/v1/namespaces/test-namespace/certificaterequests":
|
|
obj := metav1.PartialObjectMetadata{}
|
|
if err := json.NewDecoder(r.Body).Decode(&obj); err != nil {
|
|
t.Errorf("failed to decode request body: %v", err)
|
|
}
|
|
|
|
switch obj.GenerateName {
|
|
case "cmapichecker-valid-":
|
|
if test.createValidResponse != nil {
|
|
return test.createValidResponse(t, r)
|
|
}
|
|
|
|
return http.StatusOK, []byte(crAfterMutation)
|
|
case "cmapichecker-invalid-":
|
|
if test.createInvalidResponse != nil {
|
|
return test.createInvalidResponse(t, r)
|
|
}
|
|
|
|
return http.StatusNotAcceptable, []byte(`{
|
|
"kind":"Status",
|
|
"apiVersion":"v1",
|
|
"metadata":{},
|
|
"status":"Failure",
|
|
"message":"admission webhook \"webhook.cert-manager.io\" denied the request: [ERROR MESSAGE]",
|
|
"reason":"NotAcceptable",
|
|
"code":406
|
|
}`)
|
|
}
|
|
default:
|
|
}
|
|
|
|
return http.StatusNotFound, nil
|
|
}
|
|
testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
statusCode, content := mockKubernetesAPI(t, r)
|
|
w.WriteHeader(statusCode)
|
|
if content == nil {
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(content); err != nil {
|
|
t.Errorf("failed to write response: %v", err)
|
|
}
|
|
}))
|
|
t.Cleanup(testServer.Close)
|
|
|
|
restConfig := &rest.Config{
|
|
Host: testServer.URL,
|
|
}
|
|
checker, err := NewForConfigAndClient(restConfig, testServer.Client(), "test-namespace")
|
|
if err != nil {
|
|
t.Fatalf("failed to create checker: %v", err)
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
t.Logf("# check %d", i)
|
|
|
|
err = checker.Check(context.Background())
|
|
switch {
|
|
case err == nil && test.expectedError == "":
|
|
case err == nil && test.expectedError != "":
|
|
t.Errorf("expected error %q, got nil", test.expectedError)
|
|
case err.Error() != test.expectedError:
|
|
t.Errorf("expected error %q, got %q", test.expectedError, err.Error())
|
|
}
|
|
|
|
simpleErr := TranslateToSimpleError(err)
|
|
switch {
|
|
case simpleErr == nil && test.expectedSimpleError == "":
|
|
case simpleErr == nil && test.expectedSimpleError != "":
|
|
t.Errorf("expected error %q, got nil", test.expectedSimpleError)
|
|
case simpleErr.Error() != test.expectedSimpleError:
|
|
t.Errorf("expected error %q, got %q", test.expectedSimpleError, simpleErr.Error())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|