| // Copyright Istio 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 chiron |
| |
| import ( |
| "bytes" |
| "context" |
| "reflect" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| cert "k8s.io/api/certificates/v1" |
| v1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/client-go/kubernetes/fake" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/security/pkg/pki/ca" |
| "github.com/apache/dubbo-go-pixiu/security/pkg/pki/util" |
| ) |
| |
| const ( |
| exampleExpiredCert = `-----BEGIN CERTIFICATE----- |
| MIIDXjCCAkagAwIBAgIQGbJDoVfdXBsPos+p8RGqZDANBgkqhkiG9w0BAQsFADBF |
| MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 |
| ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDgxNjIxNDQzNVoXDTE5MDgxNjIx |
| NDQzNlowEzERMA8GA1UEChMISnVqdSBvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IB |
| DwAwggEKAoIBAQDYwh5Txh3pJVIjCt5JciYM4GMt3DsjF7E4JdVUUu682YpFevT8 |
| rWJVaZjZajVQaT4IIYw+kxqf0hdVLJG11OHI3OeZ1IJe5yuV+STXks/+vEDMMLuD |
| vqWZl7oFIuXR6merPAPLAmxo0U5E9kp6ftfHJMK3uj1eNp/BZE/xH8QYe86kAckd |
| QYPsz0gW1YMdpxRG1OFmbih8CdbRUjCgHHPOxbAJOIDM4xtj8M1rFgVnyH+8NucW |
| DddKy63GASUphakC73hMnoEQksbVg6rdYlnYrdPcLgmcLeO+vdI5EjbXMaXy7GMk |
| JvTJI5KRAq+jPHOZmWHd2zAGUpLRPr8EFQp3AgMBAAGjfDB6MA4GA1UdDwEB/wQE |
| AwIFoDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFKgUbNBCMgiPmanahMYt1KRO |
| dFX9MDkGA1UdEQEB/wQvMC2GK3NwaWZmZTovL2NsdXN0ZXIubG9jYWwvbnMvZGVm |
| YXVsdC9zYS9jbGllbnQwDQYJKoZIhvcNAQELBQADggEBAFEf0ZJnNz7h4MqGz720 |
| 0FShDX02WPmNSfl89gY/Q4/djTF5yfqgaDAZWf4PsDwpdMpT0eRrshpzvLrRjcd4 |
| Ev6bXGncIwnNFtXQ4dseup+kmaYZF24zpRjyoH9owwu1T5Wb1cSrpFFby5xWuoTC |
| bsKlR5CZF+dHUwc1iMaj/4kuVjvt4imM0coeaOUzOcMCruO54IqsFcJg2YA80MI+ |
| 6UiM8hj8ERDls5iBNThWKKE0yva4HFh1gj5f427NP7CSikUFXs61gytlFHEjHyco |
| lK8KK65mLIDLshz2+6lPHFXv9tEpouEUws5lhR5O9Q+9LmfBLPE7rD2aUic8EApg |
| 3TE= |
| -----END CERTIFICATE-----` |
| // The example certificate here can be generated through |
| // the following command: |
| // kubectl exec -it POD-NAME -n NAMESPACE -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt |
| // Its validity is 5 years. |
| exampleCACert1 = `-----BEGIN CERTIFICATE----- |
| MIIDCzCCAfOgAwIBAgIQbfOzhcKTldFipQ1X2WXpHDANBgkqhkiG9w0BAQsFADAv |
| MS0wKwYDVQQDEyRhNzU5YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUw |
| HhcNMTkwNTE2MjIxMTI2WhcNMjQwNTE0MjMxMTI2WjAvMS0wKwYDVQQDEyRhNzU5 |
| YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUwggEiMA0GCSqGSIb3DQEB |
| AQUAA4IBDwAwggEKAoIBAQC6sSAN80Ci0DYFpNDumGYoejMQai42g6nSKYS+ekvs |
| E7uT+eepO74wj8o6nFMNDu58+XgIsvPbWnn+3WtUjJfyiQXxmmTg8om4uY1C7R1H |
| gMsrL26pUaXZ/lTE8ZV5CnQJ9XilagY4iZKeptuZkxrWgkFBD7tr652EA3hmj+3h |
| 4sTCQ+pBJKG8BJZDNRrCoiABYBMcFLJsaKuGZkJ6KtxhQEO9QxJVaDoSvlCRGa8R |
| fcVyYQyXOZ+0VHZJQgaLtqGpiQmlFttpCwDiLfMkk3UAd79ovkhN1MCq+O5N7YVt |
| eVQWaTUqUV2tKUFvVq21Zdl4dRaq+CF5U8uOqLY/4Kg9AgMBAAGjIzAhMA4GA1Ud |
| DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCg |
| oF71Ey2b1QY22C6BXcANF1+wPzxJovFeKYAnUqwh3rF7pIYCS/adZXOKlgDBsbcS |
| MxAGnCRi1s+A7hMYj3sQAbBXttc31557lRoJrx58IeN5DyshT53t7q4VwCzuCXFT |
| 3zRHVRHQnO6LHgZx1FuKfwtkhfSXDyYU2fQYw2Hcb9krYU/alViVZdE0rENXCClq |
| xO7AQk5MJcGg6cfE5wWAKU1ATjpK4CN+RTn8v8ODLoI2SW3pfsnXxm93O+pp9HN4 |
| +O+1PQtNUWhCfh+g6BN2mYo2OEZ8qGSxDlMZej4YOdVkW8PHmFZTK0w9iJKqM5o1 |
| V6g5gZlqSoRhICK09tpc |
| -----END CERTIFICATE-----` |
| exampleCACert2 = `-----BEGIN CERTIFICATE----- |
| MIIDDDCCAfSgAwIBAgIRALsN8ND73NbYSKTZZa4jf2EwDQYJKoZIhvcNAQELBQAw |
| LzEtMCsGA1UEAxMkZTNlM2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1 |
| MB4XDTE5MDYwMTE1NTU0M1oXDTI0MDUzMDE2NTU0M1owLzEtMCsGA1UEAxMkZTNl |
| M2RlZWQtYzIyNi00OWM2LThmOTktNDU3NmRmMzQ0YWQ1MIIBIjANBgkqhkiG9w0B |
| AQEFAAOCAQ8AMIIBCgKCAQEA43oeK/hS92ANjmg50LCl3tM7eYAlBB/XgCl+bfp3 |
| KwEf+uW5yEvzSVHd2VPFI/kJJeLFrsyCRaU4FwxWcEr2Ld07DPL34oyZRRXQF0w6 |
| 4ZNSVmevBNdZLqHcoIUtR1iFJbkctE93HpGw5Kg1NXRLDu47wQtzcC3GDOEk1amu |
| mL916R2OcYEeOcyRDnlbLcsTYRvK5WBQsux4E0iu2Eo9GIajKmbxVLxA9fsmqG4i |
| /HoVkLmCg+ZRPR/66AFLPFV1J3RWp0K4HKGzBeCyd2RC+o0g8tJX3EVSuQpqzS8p |
| i2t71cYu/Sf5gt3wXsNHyzE6bF1o+acyzWvJlBym/HsbAQIDAQABoyMwITAOBgNV |
| HQ8BAf8EBAMCAgQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA |
| kJXGkFEybCp7RxUSILuIqMtKcYcQU9ulKmLSn51VrpcRHP4SH7UJ0aXMjAdRLsop |
| em7YgbvToGNingqcmSJlunR3jXDecSXJLUO1xcfw6N+B2BXRgUv8wV42btr2EV6q |
| 4HKou+MnKdrQkMUx218AT8TNPBb/Yx01m8YUS7mGUTApAhBneGEcKJ8xOznIuR5v |
| CihWQA9AmUvfixpXNpJc4vqiYErwIXrYpuwc79SRtLuO70vV7FCctz+4JPpR7mp9 |
| dHMZfGO1KXMbYT9P5bm+itlWSyrnn0qK/Cn5RHBoFyY91VcQJTgABS/z5O0pZ662 |
| sNzF00Jhi0gU7th75QT3MA== |
| -----END CERTIFICATE-----` |
| ) |
| |
| func TestNewWebhookController(t *testing.T) { |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| shouldFail bool |
| }{ |
| "invalid grade period ratio": { |
| gracePeriodRatio: 1.5, |
| dnsNames: []string{"foo"}, |
| secretNames: []string{"foo.secret"}, |
| secretNamespace: "foo.ns", |
| k8sCaCertFile: "./test-data/example-invalid-ca-cert.pem", |
| shouldFail: true, |
| }, |
| "invalid CA cert path": { |
| gracePeriodRatio: 0.6, |
| dnsNames: []string{"foo"}, |
| secretNames: []string{"foo.secret"}, |
| secretNamespace: "foo.ns", |
| k8sCaCertFile: "./invalid-path/invalid-file", |
| shouldFail: true, |
| }, |
| "valid CA cert path": { |
| gracePeriodRatio: 0.6, |
| dnsNames: []string{"foo"}, |
| secretNames: []string{"foo.secret"}, |
| secretNamespace: "foo.ns", |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| shouldFail: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| _, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if tc.shouldFail { |
| if err == nil { |
| t.Errorf("should have failed at NewWebhookController()") |
| } else { |
| // Should fail, skip the current case. |
| continue |
| } |
| } else if err != nil { |
| t.Errorf("should not fail at NewWebhookController(), err: %v", err) |
| } |
| } |
| } |
| |
| func TestUpsertSecret(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| scrtName string |
| expectFaill bool |
| }{ |
| "upsert a valid secret name should succeed": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| scrtName: "istio.webhook.foo", |
| expectFaill: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| csr := &cert.CertificateSigningRequest{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "domain-cluster.local-ns--secret-mock-secret", |
| }, |
| Status: cert.CertificateSigningRequestStatus{ |
| Certificate: []byte(exampleIssuedCert), |
| }, |
| } |
| client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr)) |
| certWatchTimeout = time.Millisecond |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Errorf("failed at creating webhook controller: %v", err) |
| continue |
| } |
| |
| err = wc.upsertSecret(tc.scrtName, tc.dnsNames[0], tc.secretNamespace) |
| if tc.expectFaill { |
| if err == nil { |
| t.Errorf("should have failed at upsertSecret") |
| } |
| continue |
| } else if err != nil { |
| t.Errorf("should not failed at upsertSecret, err: %v", err) |
| } |
| } |
| } |
| |
| func TestScrtDeleted(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| }{ |
| "recover a deleted secret should succeed": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| }, |
| } |
| |
| csr := &cert.CertificateSigningRequest{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "domain-cluster.local-ns--secret-mock-secret", |
| }, |
| Status: cert.CertificateSigningRequestStatus{ |
| Certificate: []byte(exampleIssuedCert), |
| }, |
| } |
| |
| certWatchTimeout = time.Millisecond |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr)) |
| |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Errorf("failed at creating webhook controller: %v", err) |
| continue |
| } |
| |
| _, err = client.CoreV1().Secrets(tc.secretNamespace).Create(context.TODO(), &v1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: tc.secretNames[0], |
| Labels: map[string]string{ |
| "secret": "for-testing", |
| }, |
| }, |
| }, metav1.CreateOptions{}) |
| if err != nil { |
| t.Fatalf("failed creating test secret (%v): %v", tc.secretNames[0], err) |
| } |
| scrt, err := client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt == nil { |
| t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.secretNames[0], err, scrt) |
| } |
| err = client.CoreV1().Secrets(tc.secretNamespace).Delete(context.TODO(), tc.secretNames[0], metav1.DeleteOptions{}) |
| if err != nil { |
| t.Fatalf("failed deleting test secret (%v): %v", tc.secretNames[0], err) |
| } |
| _, err = client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err == nil { |
| t.Fatal("the deleted secret should not exist") |
| } |
| |
| // The secret deleted should be recovered. |
| wc.scrtDeleted(scrt) |
| scrt, err = client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt == nil { |
| t.Fatalf("after scrtDeleted(), failed to get test secret (%v): err (%v), secret (%v)", |
| tc.secretNames[0], err, scrt) |
| } |
| } |
| } |
| |
| func TestScrtUpdated(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| certWatchTimeout = time.Millisecond |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| changeCACert bool |
| invalidNewSecret bool |
| replaceWithExpiredCert bool |
| expectUpdate bool |
| newScrtName string |
| }{ |
| "invalid new secret should not affect existing secret": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNamespace: "foo.ns", |
| secretNames: []string{"istio.webhook.foo"}, |
| invalidNewSecret: true, |
| expectUpdate: false, |
| }, |
| "non-webhook secret should not be updated": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| newScrtName: "bar", |
| invalidNewSecret: false, |
| expectUpdate: false, |
| }, |
| "expired certificate should be updated": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| replaceWithExpiredCert: true, |
| expectUpdate: true, |
| }, |
| "changing CA certificate should lead to updating secret": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| changeCACert: true, |
| replaceWithExpiredCert: false, |
| expectUpdate: true, |
| }, |
| } |
| |
| csr := &cert.CertificateSigningRequest{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "domain-cluster.local-ns--secret-mock-secret", |
| }, |
| Status: cert.CertificateSigningRequestStatus{ |
| Certificate: []byte(exampleIssuedCert), |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr)) |
| |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Errorf("failed at creating webhook controller: %v", err) |
| continue |
| } |
| |
| err = wc.upsertSecret(tc.secretNames[0], tc.dnsNames[0], tc.secretNamespace) |
| if err != nil { |
| t.Errorf("should not failed at upsertSecret, err: %v", err) |
| } |
| scrt, err := client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt == nil { |
| t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.secretNames[0], err, scrt) |
| } |
| |
| if tc.newScrtName != "" { |
| scrt.Name = tc.newScrtName |
| } |
| if tc.replaceWithExpiredCert { |
| scrt.Data[ca.CertChainFile] = []byte(exampleExpiredCert) |
| } |
| if tc.changeCACert { |
| scrt.Data[ca.RootCertFile] = []byte(exampleCACert2) |
| } |
| |
| var newScrt interface{} |
| if tc.invalidNewSecret { |
| // point to an invalid secret object |
| newScrt = &v1.ConfigMap{} |
| } else { |
| newScrt = &v1.Secret{} |
| scrt.DeepCopyInto(newScrt.(*v1.Secret)) |
| } |
| wc.scrtUpdated(scrt, newScrt) |
| |
| // scrt2 is the secret after updating, which will be compared against original scrt |
| scrt2, err := client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt2 == nil { |
| t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.secretNames[0], err, scrt2) |
| } |
| if tc.newScrtName != "" { |
| scrt2.Name = tc.newScrtName |
| } |
| if tc.expectUpdate { |
| if reflect.DeepEqual(scrt, scrt2) { |
| t.Errorf("change is expected while there is no change") |
| } |
| } else { |
| if !reflect.DeepEqual(scrt, scrt2) { |
| t.Errorf("change is not expected while there is change") |
| } |
| } |
| } |
| } |
| |
| func TestRefreshSecret(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| changeCACert bool |
| expectUpdate bool |
| }{ |
| "refresh a secret with different CA cert should succeed": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| changeCACert: true, |
| expectUpdate: true, |
| }, |
| } |
| |
| csr := &cert.CertificateSigningRequest{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "domain-cluster.local-ns--secret-mock-secret", |
| }, |
| Status: cert.CertificateSigningRequestStatus{ |
| Certificate: []byte(exampleIssuedCert), |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| client.PrependReactor("get", "certificatesigningrequests", defaultReactionFunc(csr)) |
| |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Errorf("failed at creating webhook controller: %v", err) |
| continue |
| } |
| |
| err = wc.upsertSecret(tc.secretNames[0], tc.dnsNames[0], tc.secretNamespace) |
| if err != nil { |
| t.Errorf("should not failed at upsertSecret, err: %v", err) |
| } |
| scrt, err := client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt == nil { |
| t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.secretNames[0], err, scrt) |
| } |
| |
| if tc.changeCACert { |
| scrt.Data[ca.RootCertFile] = []byte(exampleCACert2) |
| } |
| |
| newScrt := &v1.Secret{} |
| scrt.DeepCopyInto(newScrt) |
| err = wc.refreshSecret(newScrt) |
| if err != nil { |
| t.Fatalf("failed to refresh secret (%v), err: %v", newScrt, err) |
| } |
| |
| // scrt2 is the secret after refreshing, which will be compared against original scrt |
| scrt2, err := client.CoreV1().Secrets(tc.secretNamespace).Get(context.TODO(), tc.secretNames[0], metav1.GetOptions{}) |
| if err != nil || scrt2 == nil { |
| t.Fatalf("failed to get test secret (%v): err (%v), secret (%v)", tc.secretNames[0], err, scrt2) |
| } |
| if tc.expectUpdate { |
| if reflect.DeepEqual(scrt, scrt2) { |
| t.Errorf("change is expected while there is no change") |
| } |
| } else { |
| if !reflect.DeepEqual(scrt, scrt2) { |
| t.Errorf("change is not expected while there is change") |
| } |
| } |
| } |
| } |
| |
| func TestCleanUpCertGen(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| }{ |
| "clean up a CSR should succeed": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo"}, |
| secretNamespace: "foo.ns", |
| }, |
| } |
| |
| csrName := "test-csr" |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Fatalf("failed at creating webhook controller: %v", err) |
| } |
| |
| options := util.CertOptions{ |
| Host: "test-host", |
| RSAKeySize: keySize, |
| IsDualUse: false, |
| PKCS8Key: false, |
| } |
| csrPEM, _, err := util.GenCSR(options) |
| if err != nil { |
| t.Fatalf("CSR generation error (%v)", err) |
| } |
| |
| k8sCSR := &cert.CertificateSigningRequest{ |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "CertificateSigningRequest", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: csrName, |
| }, |
| Spec: cert.CertificateSigningRequestSpec{ |
| Request: csrPEM, |
| SignerName: "test-signer", |
| Usages: []cert.KeyUsage{ |
| cert.UsageDigitalSignature, |
| cert.UsageKeyEncipherment, |
| cert.UsageServerAuth, |
| cert.UsageClientAuth, |
| }, |
| }, |
| } |
| _, err = wc.clientset.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), k8sCSR, metav1.CreateOptions{}) |
| if err != nil { |
| t.Fatalf("error when creating CSR: %v", err) |
| } |
| |
| csr, err := wc.clientset.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{}) |
| if err != nil || csr == nil { |
| t.Fatalf("failed to get CSR: name (%v), err (%v), CSR (%v)", csrName, err, csr) |
| } |
| |
| // The CSR should be deleted. |
| err = cleanUpCertGen(wc.clientset, true, csrName) |
| if err != nil { |
| t.Errorf("cleanUpCertGen returns an error: %v", err) |
| } |
| _, err = wc.clientset.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), csrName, metav1.GetOptions{}) |
| if err == nil { |
| t.Fatalf("should failed at getting CSR: name (%v)", csrName) |
| } |
| } |
| } |
| |
| func TestIsWebhookSecret(t *testing.T) { |
| dnsNames := []string{"foo", "bar"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| namespace string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| scrtName string |
| scrtNameSpace string |
| expectedRet bool |
| }{ |
| "a valid webhook secret in valid namespace": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo", "istio.webhook.bar"}, |
| secretNamespace: "ns.foo", |
| namespace: "ns.foo", |
| scrtName: "istio.webhook.foo", |
| scrtNameSpace: "ns.foo", |
| expectedRet: true, |
| }, |
| "an invalid webhook secret in valid namespace": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| namespace: "ns.foo", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo", "istio.webhook.bar"}, |
| secretNamespace: "ns.foo", |
| scrtName: "istio.webhook.invalid", |
| scrtNameSpace: "ns.foo", |
| expectedRet: false, |
| }, |
| "a valid webhook secret in invalid namespace": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| namespace: "ns.foo", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo", "istio.webhook.bar"}, |
| secretNamespace: "ns.foo", |
| scrtName: "istio.webhook.foo", |
| scrtNameSpace: "ns.invalid", |
| expectedRet: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, |
| tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Fatalf("failed to create a webhook controller: %v", err) |
| } |
| |
| ret := wc.isWebhookSecret(tc.scrtName, tc.scrtNameSpace) |
| if tc.expectedRet != ret { |
| t.Fatalf("expected result (%v) differs from the actual result (%v)", tc.expectedRet, ret) |
| } |
| // Sleep for watchers to release the file handles |
| time.Sleep(10 * time.Millisecond) |
| } |
| } |
| |
| func TestGetCACert(t *testing.T) { |
| dnsNames := []string{"foo"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| namespace string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| expectFail bool |
| }{ |
| "getCACert should succeed for a valid certificate": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| namespace: "foo.ns", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webook.foo"}, |
| secretNamespace: "foo.ns", |
| expectFail: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| // If the CA cert. is invalid, NewWebhookController will fail. |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, |
| tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, tc.secretNamespace, "test-issuer") |
| if err != nil { |
| t.Fatalf("failed at creating webhook controller: %v", err) |
| } |
| |
| cert, err := wc.getCACert() |
| if !tc.expectFail { |
| if err != nil { |
| t.Errorf("failed to get CA cert: %v", err) |
| } else if !bytes.Equal(cert, []byte(exampleCACert1)) { |
| t.Errorf("the CA certificate read does not match the actual certificate") |
| } |
| } else if err == nil { |
| t.Error("expect failure on getting CA cert but succeeded") |
| } |
| } |
| } |
| |
| func TestGetDNSName(t *testing.T) { |
| dnsNames := []string{"foo", "bar", "baz"} |
| |
| testCases := map[string]struct { |
| gracePeriodRatio float32 |
| minGracePeriod time.Duration |
| k8sCaCertFile string |
| dnsNames []string |
| secretNames []string |
| secretNamespace string |
| scrtName string |
| expectFound bool |
| expectedSvcName string |
| }{ |
| "a service corresponding to a secret exists": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo", "istio.webhook.bar", "istio.webhoo.baz"}, |
| secretNamespace: "foo.ns", |
| scrtName: "istio.webhook.foo", |
| expectFound: true, |
| expectedSvcName: "foo", |
| }, |
| "a service corresponding to a secret does not exists": { |
| gracePeriodRatio: 0.6, |
| k8sCaCertFile: "./test-data/example-ca-cert.pem", |
| dnsNames: dnsNames, |
| secretNames: []string{"istio.webhook.foo", "istio.webhook.bar", "istio.webhoo.baz"}, |
| secretNamespace: "foo.ns", |
| scrtName: "istio.webhook.barr", |
| expectFound: false, |
| expectedSvcName: "bar", |
| }, |
| } |
| |
| for _, tc := range testCases { |
| client := fake.NewSimpleClientset() |
| wc, err := NewWebhookController(tc.gracePeriodRatio, tc.minGracePeriod, |
| client, |
| tc.k8sCaCertFile, tc.secretNames, tc.dnsNames, "foo.ns", "test-issuer") |
| if err != nil { |
| t.Errorf("failed to create a webhook controller: %v", err) |
| } |
| |
| ret, found := wc.getDNSName(tc.scrtName) |
| if tc.expectFound != found { |
| t.Errorf("expected found (%v) differs from the actual found (%v)", tc.expectFound, found) |
| continue |
| } |
| if found && tc.expectedSvcName != ret { |
| t.Errorf("the service name (%v) returned is not as expcted (%v)", ret, tc.expectedSvcName) |
| } |
| } |
| } |