| // 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 xds |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| "github.com/google/go-cmp/cmp" |
| corev1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/client-go/kubernetes/fake" |
| k8stesting "k8s.io/client-go/testing" |
| ) |
| |
| import ( |
| credentials "github.com/apache/dubbo-go-pixiu/pilot/pkg/credentials/kube" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| v3 "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/v3" |
| "github.com/apache/dubbo-go-pixiu/pilot/test/xdstest" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk" |
| "github.com/apache/dubbo-go-pixiu/pkg/kube" |
| "github.com/apache/dubbo-go-pixiu/pkg/spiffe" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/env" |
| ) |
| |
| func makeSecret(name string, data map[string]string) *corev1.Secret { |
| bdata := map[string][]byte{} |
| for k, v := range data { |
| bdata[k] = []byte(v) |
| } |
| return &corev1.Secret{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: name, |
| Namespace: "dubbo-system", |
| }, |
| Data: bdata, |
| } |
| } |
| |
| var ( |
| certDir = filepath.Join(env.IstioSrc, "./tests/testdata/certs") |
| genericCert = makeSecret("generic", map[string]string{ |
| credentials.GenericScrtCert: readFile(filepath.Join(certDir, "default/cert-chain.pem")), |
| credentials.GenericScrtKey: readFile(filepath.Join(certDir, "default/key.pem")), |
| }) |
| genericMtlsCert = makeSecret("generic-mtls", map[string]string{ |
| credentials.GenericScrtCert: readFile(filepath.Join(certDir, "dns/cert-chain.pem")), |
| credentials.GenericScrtKey: readFile(filepath.Join(certDir, "dns/key.pem")), |
| credentials.GenericScrtCaCert: readFile(filepath.Join(certDir, "dns/root-cert.pem")), |
| }) |
| genericMtlsCertSplit = makeSecret("generic-mtls-split", map[string]string{ |
| credentials.GenericScrtCert: readFile(filepath.Join(certDir, "mountedcerts-client/cert-chain.pem")), |
| credentials.GenericScrtKey: readFile(filepath.Join(certDir, "mountedcerts-client/key.pem")), |
| }) |
| genericMtlsCertSplitCa = makeSecret("generic-mtls-split-cacert", map[string]string{ |
| credentials.GenericScrtCaCert: readFile(filepath.Join(certDir, "mountedcerts-client/root-cert.pem")), |
| }) |
| ) |
| |
| func readFile(name string) string { |
| cacert, _ := os.ReadFile(name) |
| return string(cacert) |
| } |
| |
| func TestGenerate(t *testing.T) { |
| type Expected struct { |
| Key string |
| Cert string |
| CaCert string |
| } |
| allResources := []string{ |
| "kubernetes://generic", "kubernetes://generic-mtls", "kubernetes://generic-mtls-cacert", |
| "kubernetes://generic-mtls-split", "kubernetes://generic-mtls-split-cacert", |
| } |
| cases := []struct { |
| name string |
| proxy *model.Proxy |
| resources []string |
| request *model.PushRequest |
| expect map[string]Expected |
| accessReviewResponse func(action k8stesting.Action) (bool, runtime.Object, error) |
| }{ |
| { |
| name: "simple", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: []string{"kubernetes://generic"}, |
| request: &model.PushRequest{Full: true}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| }, |
| }, |
| { |
| name: "sidecar", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}}, |
| resources: []string{"kubernetes://generic"}, |
| request: &model.PushRequest{Full: true}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| }, |
| }, |
| { |
| name: "unauthenticated", |
| proxy: &model.Proxy{Type: model.Router}, |
| resources: []string{"kubernetes://generic"}, |
| request: &model.PushRequest{Full: true}, |
| expect: map[string]Expected{}, |
| }, |
| { |
| name: "multiple", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: allResources, |
| request: &model.PushRequest{Full: true}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls": { |
| Key: string(genericMtlsCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-cacert": { |
| CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]), |
| }, |
| "kubernetes://generic-mtls-split": { |
| Key: string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-split-cacert": { |
| CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]), |
| }, |
| }, |
| }, |
| { |
| name: "full push with updates", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: []string{"kubernetes://generic", "kubernetes://generic-mtls", "kubernetes://generic-mtls-cacert"}, |
| request: &model.PushRequest{Full: true, ConfigsUpdated: map[model.ConfigKey]struct{}{ |
| {Name: "generic-mtls", Namespace: "dubbo-system", Kind: gvk.Secret}: {}, |
| }}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls": { |
| Key: string(genericMtlsCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-cacert": { |
| CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]), |
| }, |
| }, |
| }, |
| { |
| name: "incremental push with updates", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: allResources, |
| request: &model.PushRequest{Full: false, ConfigsUpdated: map[model.ConfigKey]struct{}{ |
| {Name: "generic", Namespace: "dubbo-system", Kind: gvk.Secret}: {}, |
| }}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| }, |
| }, |
| { |
| name: "incremental push with updates - mtls", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: allResources, |
| request: &model.PushRequest{Full: false, ConfigsUpdated: map[model.ConfigKey]struct{}{ |
| {Name: "generic-mtls", Namespace: "dubbo-system", Kind: gvk.Secret}: {}, |
| }}, |
| expect: map[string]Expected{ |
| "kubernetes://generic-mtls": { |
| Key: string(genericMtlsCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCert.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-cacert": { |
| CaCert: string(genericMtlsCert.Data[credentials.GenericScrtCaCert]), |
| }, |
| }, |
| }, |
| { |
| name: "incremental push with updates - mtls split", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: allResources, |
| request: &model.PushRequest{Full: false, ConfigsUpdated: map[model.ConfigKey]struct{}{ |
| {Name: "generic-mtls-split", Namespace: "dubbo-system", Kind: gvk.Secret}: {}, |
| }}, |
| expect: map[string]Expected{ |
| "kubernetes://generic-mtls-split": { |
| Key: string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-split-cacert": { |
| CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]), |
| }, |
| }, |
| }, |
| { |
| name: "incremental push with updates - mtls split ca update", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: allResources, |
| request: &model.PushRequest{Full: false, ConfigsUpdated: map[model.ConfigKey]struct{}{ |
| {Name: "generic-mtls-split-cacert", Namespace: "dubbo-system", Kind: gvk.Secret}: {}, |
| }}, |
| expect: map[string]Expected{ |
| "kubernetes://generic-mtls-split": { |
| Key: string(genericMtlsCertSplit.Data[credentials.GenericScrtKey]), |
| Cert: string(genericMtlsCertSplit.Data[credentials.GenericScrtCert]), |
| }, |
| "kubernetes://generic-mtls-split-cacert": { |
| CaCert: string(genericMtlsCertSplitCa.Data[credentials.GenericScrtCaCert]), |
| }, |
| }, |
| }, |
| { |
| // If an unknown resource is request, we return all the ones we do know about |
| name: "unknown", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: []string{"kubernetes://generic", "foo://invalid", "kubernetes://not-found", "default", "builtin://"}, |
| request: &model.PushRequest{Full: true}, |
| expect: map[string]Expected{ |
| "kubernetes://generic": { |
| Key: string(genericCert.Data[credentials.GenericScrtKey]), |
| Cert: string(genericCert.Data[credentials.GenericScrtCert]), |
| }, |
| }, |
| }, |
| { |
| // proxy without authorization |
| name: "unauthorized", |
| proxy: &model.Proxy{VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, Type: model.Router}, |
| resources: []string{"kubernetes://generic"}, |
| request: &model.PushRequest{Full: true}, |
| // Should get a response, but it will be empty |
| expect: map[string]Expected{}, |
| accessReviewResponse: func(action k8stesting.Action) (bool, runtime.Object, error) { |
| return true, nil, errors.New("not authorized") |
| }, |
| }, |
| } |
| for _, tt := range cases { |
| t.Run(tt.name, func(t *testing.T) { |
| if tt.proxy.Metadata == nil { |
| tt.proxy.Metadata = &model.NodeMetadata{} |
| } |
| tt.proxy.Metadata.ClusterID = "Kubernetes" |
| s := NewFakeDiscoveryServer(t, FakeOptions{ |
| KubernetesObjects: []runtime.Object{genericCert, genericMtlsCert, genericMtlsCertSplit, genericMtlsCertSplitCa}, |
| }) |
| cc := s.KubeClient().Kube().(*fake.Clientset) |
| |
| cc.Fake.Lock() |
| if tt.accessReviewResponse != nil { |
| cc.Fake.PrependReactor("create", "subjectaccessreviews", tt.accessReviewResponse) |
| } else { |
| credentials.DisableAuthorizationForTest(cc) |
| } |
| cc.Fake.Unlock() |
| |
| gen := s.Discovery.Generators[v3.SecretType] |
| tt.request.Start = time.Now() |
| secrets, _, _ := gen.Generate(s.SetupProxy(tt.proxy), &model.WatchedResource{ResourceNames: tt.resources}, tt.request) |
| raw := xdstest.ExtractTLSSecrets(t, model.ResourcesToAny(secrets)) |
| |
| got := map[string]Expected{} |
| for _, scrt := range raw { |
| got[scrt.Name] = Expected{ |
| Key: string(scrt.GetTlsCertificate().GetPrivateKey().GetInlineBytes()), |
| Cert: string(scrt.GetTlsCertificate().GetCertificateChain().GetInlineBytes()), |
| CaCert: string(scrt.GetValidationContext().GetTrustedCa().GetInlineBytes()), |
| } |
| } |
| if diff := cmp.Diff(got, tt.expect); diff != "" { |
| t.Fatal(diff) |
| } |
| }) |
| } |
| } |
| |
| // TestCaching ensures we don't have cross-proxy cache generation issues. This is split from TestGenerate |
| // since it is order dependant. |
| // Regression test for https://github.com/istio/istio/issues/33368 |
| func TestCaching(t *testing.T) { |
| s := NewFakeDiscoveryServer(t, FakeOptions{ |
| KubernetesObjects: []runtime.Object{genericCert}, |
| KubeClientModifier: func(c kube.Client) { |
| cc := c.Kube().(*fake.Clientset) |
| credentials.DisableAuthorizationForTest(cc) |
| }, |
| }) |
| gen := s.Discovery.Generators[v3.SecretType] |
| |
| fullPush := &model.PushRequest{Full: true, Start: time.Now()} |
| istiosystem := &model.Proxy{ |
| Metadata: &model.NodeMetadata{ClusterID: "Kubernetes"}, |
| VerifiedIdentity: &spiffe.Identity{Namespace: "dubbo-system"}, |
| Type: model.Router, |
| ConfigNamespace: "dubbo-system", |
| } |
| otherNamespace := &model.Proxy{ |
| Metadata: &model.NodeMetadata{ClusterID: "Kubernetes"}, |
| VerifiedIdentity: &spiffe.Identity{Namespace: "other-namespace"}, |
| Type: model.Router, |
| ConfigNamespace: "other-namespace", |
| } |
| |
| secrets, _, _ := gen.Generate(s.SetupProxy(istiosystem), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush) |
| raw := xdstest.ExtractTLSSecrets(t, model.ResourcesToAny(secrets)) |
| if len(raw) != 1 { |
| t.Fatalf("failed to get expected secrets for authorized proxy: %v", raw) |
| } |
| |
| // We should not get secret returned, even though we are asking for the same one |
| secrets, _, _ = gen.Generate(s.SetupProxy(otherNamespace), &model.WatchedResource{ResourceNames: []string{"kubernetes://generic"}}, fullPush) |
| raw = xdstest.ExtractTLSSecrets(t, model.ResourcesToAny(secrets)) |
| if len(raw) != 0 { |
| t.Fatalf("failed to get expected secrets for unauthorized proxy: %v", raw) |
| } |
| } |
| |
| func TestAtMostNJoin(t *testing.T) { |
| tests := []struct { |
| data []string |
| limit int |
| want string |
| }{ |
| { |
| []string{"a", "b", "c"}, |
| 2, |
| "a, and 2 others", |
| }, |
| { |
| []string{"a", "b", "c"}, |
| 4, |
| "a, b, c", |
| }, |
| { |
| []string{"a", "b", "c"}, |
| 1, |
| "a, b, c", |
| }, |
| { |
| []string{"a", "b", "c"}, |
| 0, |
| "a, b, c", |
| }, |
| { |
| []string{}, |
| 3, |
| "", |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(fmt.Sprintf("%s-%d", strings.Join(tt.data, "-"), tt.limit), func(t *testing.T) { |
| if got := atMostNJoin(tt.data, tt.limit); got != tt.want { |
| t.Errorf("got %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |