| // 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 inject |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "testing" |
| ) |
| |
| import ( |
| jsonpatch "github.com/evanphx/json-patch/v5" |
| openshiftv1 "github.com/openshift/api/apps/v1" |
| "google.golang.org/protobuf/types/known/wrapperspb" |
| "istio.io/api/annotation" |
| meshconfig "istio.io/api/mesh/v1alpha1" |
| v1beta12 "istio.io/api/networking/v1beta1" |
| "k8s.io/api/admission/v1beta1" |
| appsv1 "k8s.io/api/apps/v1" |
| batchv1 "k8s.io/api/batch/v1" |
| corev1 "k8s.io/api/core/v1" |
| 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/schema" |
| k8syaml "k8s.io/apimachinery/pkg/util/yaml" |
| "sigs.k8s.io/yaml" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/manifest" |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/name" |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/util/clog" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/test/util" |
| "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/mesh" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/schema/gvk" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/util/file" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/util/retry" |
| sutil "github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/util" |
| ) |
| |
| const yamlSeparator = "\n---" |
| |
| var minimalSidecarTemplate = &Config{ |
| Policy: InjectionPolicyEnabled, |
| DefaultTemplates: []string{SidecarTemplateName}, |
| RawTemplates: map[string]string{SidecarTemplateName: ` |
| spec: |
| initContainers: |
| - name: istio-init |
| containers: |
| - name: istio-proxy |
| volumes: |
| - name: istio-envoy |
| imagePullSecrets: |
| - name: istio-image-pull-secrets |
| `}, |
| } |
| |
| func parseToLabelSelector(t *testing.T, selector string) *metav1.LabelSelector { |
| result, err := metav1.ParseToLabelSelector(selector) |
| if err != nil { |
| t.Errorf("Invalid selector %v: %v", selector, err) |
| } |
| |
| return result |
| } |
| |
| func TestInjectRequired(t *testing.T) { |
| podSpec := &corev1.PodSpec{} |
| podSpecHostNetwork := &corev1.PodSpec{ |
| HostNetwork: true, |
| } |
| cases := []struct { |
| config *Config |
| podSpec *corev1.PodSpec |
| meta metav1.ObjectMeta |
| want bool |
| }{ |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "no-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "default-policy", |
| Namespace: "test-namespace", |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "force-on-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "force-off-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "no-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "default-policy", |
| Namespace: "test-namespace", |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "force-on-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "force-off-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| }, |
| podSpec: podSpecHostNetwork, |
| meta: metav1.ObjectMeta{ |
| Name: "force-off-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: "wrong_policy", |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "wrong-policy", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-always-inject-no-labels", |
| Namespace: "test-namespace", |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-always-inject-with-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "bar1"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-inject-no-labels", |
| Namespace: "test-namespace", |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-inject-with-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "bar"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| NeverInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-never-inject-no-labels", |
| Namespace: "test-namespace", |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| NeverInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-never-inject-with-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "bar"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| NeverInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-never-inject-no-labels", |
| Namespace: "test-namespace", |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| NeverInjectSelector: []metav1.LabelSelector{{MatchLabels: map[string]string{"foo": "bar"}}}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-never-inject-with-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "bar"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-never-inject-with-empty-label", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-inject-with-empty-label", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "always")}, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "never")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-never-inject-with-label-returns-true", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"always": "bar", "foo2": "bar2"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "always")}, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "never")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-never-inject-with-label-returns-false", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"never": "bar", "foo2": "bar2"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "always")}, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "never")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-always-never-inject-with-both-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"always": "bar", "never": "bar2"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-annotation-true-never-inject", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-annotation-false-always-inject", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-annotation-false-always-inject", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyEnabled, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo"), *parseToLabelSelector(t, "bar")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-never-inject-multiple-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"label1": "", "bar": "anything"}, |
| }, |
| want: false, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| AlwaysInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo"), *parseToLabelSelector(t, "bar")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-enabled-always-inject-multiple-labels", |
| Namespace: "test-namespace", |
| Labels: map[string]string{"label1": "", "bar": "anything"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| NeverInjectSelector: []metav1.LabelSelector{*parseToLabelSelector(t, "foo")}, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-annotation-true-never-inject", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| Labels: map[string]string{"foo": "", "foo2": "bar2"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-label-enabled", |
| Namespace: "test-namespace", |
| // Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| Labels: map[string]string{annotation.SidecarInject.Name: "true"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-both-enabled", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| Labels: map[string]string{annotation.SidecarInject.Name: "true"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-label-enabled-annotation-disabled", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "false"}, |
| Labels: map[string]string{annotation.SidecarInject.Name: "true"}, |
| }, |
| want: true, |
| }, |
| { |
| config: &Config{ |
| Policy: InjectionPolicyDisabled, |
| }, |
| podSpec: podSpec, |
| meta: metav1.ObjectMeta{ |
| Name: "policy-disabled-label-disabled-annotation-enabled", |
| Namespace: "test-namespace", |
| Annotations: map[string]string{annotation.SidecarInject.Name: "true"}, |
| Labels: map[string]string{annotation.SidecarInject.Name: "false"}, |
| }, |
| want: false, |
| }, |
| } |
| |
| for _, c := range cases { |
| if got := injectRequired(IgnoredNamespaces.UnsortedList(), c.config, c.podSpec, c.meta); got != c.want { |
| t.Errorf("injectRequired(%v, %v) got %v want %v", c.config, c.meta, got, c.want) |
| } |
| } |
| } |
| |
| func simulateOwnerRef(m metav1.ObjectMeta, name string, gvk schema.GroupVersionKind) metav1.ObjectMeta { |
| controller := true |
| m.GenerateName = name |
| m.OwnerReferences = []metav1.OwnerReference{{ |
| APIVersion: gvk.GroupVersion().String(), |
| Kind: gvk.Kind, |
| Name: name, |
| Controller: &controller, |
| }} |
| return m |
| } |
| |
| func objectToPod(t testing.TB, obj runtime.Object) *corev1.Pod { |
| gvk := obj.GetObjectKind().GroupVersionKind() |
| defaultConversion := func(template corev1.PodTemplateSpec, name string) *corev1.Pod { |
| template.ObjectMeta = simulateOwnerRef(template.ObjectMeta, name, gvk) |
| return &corev1.Pod{ |
| ObjectMeta: template.ObjectMeta, |
| Spec: template.Spec, |
| } |
| } |
| switch o := obj.(type) { |
| case *corev1.Pod: |
| return o |
| case *batchv1.CronJob: |
| o.Spec.JobTemplate.Spec.Template.ObjectMeta = simulateOwnerRef(o.Spec.JobTemplate.Spec.Template.ObjectMeta, o.Name, gvk) |
| return &corev1.Pod{ |
| ObjectMeta: o.Spec.JobTemplate.Spec.Template.ObjectMeta, |
| Spec: o.Spec.JobTemplate.Spec.Template.Spec, |
| } |
| case *appsv1.DaemonSet: |
| return defaultConversion(o.Spec.Template, o.Name) |
| case *appsv1.ReplicaSet: |
| return defaultConversion(o.Spec.Template, o.Name) |
| case *corev1.ReplicationController: |
| return defaultConversion(*o.Spec.Template, o.Name) |
| case *appsv1.StatefulSet: |
| return defaultConversion(o.Spec.Template, o.Name) |
| case *batchv1.Job: |
| return defaultConversion(o.Spec.Template, o.Name) |
| case *openshiftv1.DeploymentConfig: |
| return defaultConversion(*o.Spec.Template, o.Name) |
| case *appsv1.Deployment: |
| // Deployment is special since its a double nested resource |
| rsgvk := schema.GroupVersionKind{Kind: "ReplicaSet", Group: "apps", Version: "v1"} |
| o.Spec.Template.ObjectMeta = simulateOwnerRef(o.Spec.Template.ObjectMeta, o.Name+"-fake", rsgvk) |
| o.Spec.Template.ObjectMeta.GenerateName += "-" |
| if o.Spec.Template.ObjectMeta.Labels == nil { |
| o.Spec.Template.ObjectMeta.Labels = map[string]string{} |
| } |
| o.Spec.Template.ObjectMeta.Labels["pod-template-hash"] = "fake" |
| return &corev1.Pod{ |
| ObjectMeta: o.Spec.Template.ObjectMeta, |
| Spec: o.Spec.Template.Spec, |
| } |
| } |
| t.Fatalf("unknown type: %T", obj) |
| return nil |
| } |
| |
| func readInjectionSettings(t testing.TB, fname string) (*Config, ValuesConfig, *meshconfig.MeshConfig) { |
| values := file.AsStringOrFail(t, filepath.Join("testdata", "inputs", fname+".values.gen.yaml")) |
| template := file.AsBytesOrFail(t, filepath.Join("testdata", "inputs", fname+".template.gen.yaml")) |
| meshc := file.AsStringOrFail(t, filepath.Join("testdata", "inputs", fname+".mesh.gen.yaml")) |
| |
| vc, err := NewValuesConfig(values) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cfg, err := UnmarshalConfig(template) |
| if err != nil { |
| t.Fatalf("failed to unmarshal injectionConfig: %v", err) |
| } |
| meshConfig, err := mesh.ApplyMeshConfig(meshc, mesh.DefaultMeshConfig()) |
| if err != nil { |
| t.Fatalf("failed to unmarshal meshconfig: %v", err) |
| } |
| |
| return &cfg, vc, meshConfig |
| } |
| |
| // loadInjectionSettings will render the charts using the operator, with given yaml overrides. |
| // This allows us to fully simulate what will actually happen at run time. |
| func writeInjectionSettings(t testing.TB, fname string, setFlags []string, inFilePath string) { |
| // add --set installPackagePath=<path to charts snapshot> |
| setFlags = append(setFlags, "installPackagePath="+defaultInstallPackageDir(), "profile=empty", "components.pilot.enabled=true") |
| var inFilenames []string |
| if inFilePath != "" { |
| inFilenames = []string{"testdata/inject/" + inFilePath} |
| } |
| |
| l := clog.NewConsoleLogger(os.Stdout, os.Stderr, nil) |
| manifests, _, err := manifest.GenManifests(inFilenames, setFlags, false, nil, nil, l) |
| if err != nil { |
| t.Fatalf("failed to generate manifests: %v", err) |
| } |
| for _, mlist := range manifests[name.PilotComponentName] { |
| for _, object := range strings.Split(mlist, yamlSeparator) { |
| if len(object) == 0 { |
| continue |
| } |
| r := bytes.NewReader([]byte(object)) |
| decoder := k8syaml.NewYAMLOrJSONDecoder(r, 1024) |
| |
| out := &unstructured.Unstructured{} |
| err := decoder.Decode(out) |
| if err != nil { |
| t.Fatalf("error decoding object %q: %v", object, err) |
| } |
| if out.GetName() == "istio-sidecar-injector" && (out.GroupVersionKind() == schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}) { |
| data, ok := out.Object["data"].(map[string]interface{}) |
| if !ok { |
| t.Fatalf("failed to convert %v", out) |
| } |
| config, ok := data["config"].(string) |
| if !ok { |
| t.Fatalf("failed to config %v", data) |
| } |
| vs, ok := data["values"].(string) |
| if !ok { |
| t.Fatalf("failed to config %v", data) |
| } |
| if err := os.WriteFile(filepath.Join("testdata", "inputs", fname+".values.gen.yaml"), []byte(vs), 0o644); err != nil { |
| t.Fatal(err) |
| } |
| if err := os.WriteFile(filepath.Join("testdata", "inputs", fname+".template.gen.yaml"), []byte(config), 0o644); err != nil { |
| t.Fatal(err) |
| } |
| } else if out.GetName() == "istio" && (out.GroupVersionKind() == schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}) { |
| data, ok := out.Object["data"].(map[string]interface{}) |
| if !ok { |
| t.Fatalf("failed to convert %v", out) |
| } |
| meshdata, ok := data["mesh"].(string) |
| if !ok { |
| t.Fatalf("failed to get meshconfig %v", data) |
| } |
| if err := os.WriteFile(filepath.Join("testdata", "inputs", fname+".mesh.gen.yaml"), []byte(meshdata), 0o644); err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| } |
| } |
| |
| func splitYamlFile(yamlFile string, t *testing.T) [][]byte { |
| t.Helper() |
| yamlBytes := util.ReadFile(t, yamlFile) |
| return splitYamlBytes(yamlBytes, t) |
| } |
| |
| func splitYamlBytes(yaml []byte, t *testing.T) [][]byte { |
| t.Helper() |
| stringParts := strings.Split(string(yaml), yamlSeparator) |
| byteParts := make([][]byte, 0) |
| for _, stringPart := range stringParts { |
| byteParts = append(byteParts, getInjectableYamlDocs(stringPart, t)...) |
| } |
| if len(byteParts) == 0 { |
| t.Fatalf("Found no injectable parts") |
| } |
| return byteParts |
| } |
| |
| func getInjectableYamlDocs(yamlDoc string, t *testing.T) [][]byte { |
| t.Helper() |
| m := make(map[string]interface{}) |
| if err := yaml.Unmarshal([]byte(yamlDoc), &m); err != nil { |
| t.Fatal(err) |
| } |
| switch m["kind"] { |
| case "Deployment", "DeploymentConfig", "DaemonSet", "StatefulSet", "Job", "ReplicaSet", |
| "ReplicationController", "CronJob", "Pod": |
| return [][]byte{[]byte(yamlDoc)} |
| case "List": |
| // Split apart the list into separate yaml documents. |
| out := make([][]byte, 0) |
| list := metav1.List{} |
| if err := yaml.Unmarshal([]byte(yamlDoc), &list); err != nil { |
| t.Fatal(err) |
| } |
| for _, i := range list.Items { |
| iout, err := yaml.Marshal(i) |
| if err != nil { |
| t.Fatal(err) |
| } |
| injectables := getInjectableYamlDocs(string(iout), t) |
| out = append(out, injectables...) |
| } |
| return out |
| default: |
| // No injectable parts. |
| return [][]byte{} |
| } |
| } |
| |
| func convertToJSON(i interface{}, t *testing.T) []byte { |
| t.Helper() |
| outputJSON, err := json.Marshal(i) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return prettyJSON(outputJSON, t) |
| } |
| |
| func prettyJSON(inputJSON []byte, t *testing.T) []byte { |
| t.Helper() |
| // Pretty-print the JSON |
| var prettyBuffer bytes.Buffer |
| if err := json.Indent(&prettyBuffer, inputJSON, "", " "); err != nil { |
| t.Fatalf(err.Error()) |
| } |
| return prettyBuffer.Bytes() |
| } |
| |
| func applyJSONPatch(input, patch []byte, t *testing.T) []byte { |
| t.Helper() |
| p, err := jsonpatch.DecodePatch(patch) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| patchedJSON, err := p.Apply(input) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return prettyJSON(patchedJSON, t) |
| } |
| |
| func jsonToUnstructured(obj []byte, t *testing.T) *unstructured.Unstructured { |
| r := bytes.NewReader(obj) |
| decoder := k8syaml.NewYAMLOrJSONDecoder(r, 1024) |
| |
| out := &unstructured.Unstructured{} |
| err := decoder.Decode(out) |
| if err != nil { |
| t.Fatalf("error decoding object: %v", err) |
| } |
| return out |
| } |
| |
| func normalizeAndCompareDeployments(got, want *corev1.Pod, ignoreIstioMetaJSONAnnotationsEnv bool, t *testing.T) error { |
| t.Helper() |
| // Scrub unimportant fields that tend to differ. |
| delete(got.Annotations, annotation.SidecarStatus.Name) |
| delete(want.Annotations, annotation.SidecarStatus.Name) |
| |
| for _, c := range got.Spec.Containers { |
| for _, env := range c.Env { |
| if env.ValueFrom != nil { |
| env.ValueFrom.FieldRef.APIVersion = "" |
| } |
| // check if metajson is encoded correctly |
| if strings.HasPrefix(env.Name, "ISTIO_METAJSON_") { |
| var mm map[string]string |
| if err := json.Unmarshal([]byte(env.Value), &mm); err != nil { |
| t.Fatalf("unable to unmarshal %s: %v", env.Value, err) |
| } |
| } |
| } |
| } |
| |
| if ignoreIstioMetaJSONAnnotationsEnv { |
| removeContainerEnvEntry(got, "ISTIO_METAJSON_ANNOTATIONS") |
| removeContainerEnvEntry(want, "ISTIO_METAJSON_ANNOTATIONS") |
| } |
| |
| gotString, err := json.Marshal(got) |
| if err != nil { |
| t.Fatal(err) |
| } |
| wantString, err := json.Marshal(want) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| return util.Compare(gotString, wantString) |
| } |
| |
| func removeContainerEnvEntry(pod *corev1.Pod, envVarName string) { |
| for i, c := range pod.Spec.Containers { |
| for j, v := range c.Env { |
| if v.Name == envVarName { |
| pod.Spec.Containers[i].Env = append(c.Env[:j], c.Env[j+1:]...) |
| break |
| } |
| } |
| } |
| } |
| |
| func makeTestData(t testing.TB, skip bool, apiVersion string) []byte { |
| t.Helper() |
| |
| pod := corev1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "test", |
| Annotations: map[string]string{}, |
| }, |
| Spec: corev1.PodSpec{ |
| Volumes: []corev1.Volume{{Name: "v0"}}, |
| InitContainers: []corev1.Container{{Name: "c0"}}, |
| Containers: []corev1.Container{{Name: "c1"}}, |
| ImagePullSecrets: []corev1.LocalObjectReference{{Name: "p0"}}, |
| }, |
| } |
| |
| if skip { |
| pod.ObjectMeta.Annotations[annotation.SidecarInject.Name] = "false" |
| } |
| |
| raw, err := json.Marshal(&pod) |
| if err != nil { |
| t.Fatalf("Could not create test pod: %v", err) |
| } |
| |
| review := v1beta1.AdmissionReview{ |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "AdmissionReview", |
| APIVersion: fmt.Sprintf("admission.k8s.io/%s", apiVersion), |
| }, |
| Request: &v1beta1.AdmissionRequest{ |
| Kind: metav1.GroupVersionKind{ |
| Group: v1beta1.GroupName, |
| Version: apiVersion, |
| Kind: "AdmissionRequest", |
| }, |
| Object: runtime.RawExtension{ |
| Raw: raw, |
| }, |
| Operation: v1beta1.Create, |
| }, |
| } |
| reviewJSON, err := json.Marshal(review) |
| if err != nil { |
| t.Fatalf("Failed to create AdmissionReview: %v", err) |
| } |
| return reviewJSON |
| } |
| |
| func createWebhook(t testing.TB, cfg *Config, pcResources int) *Webhook { |
| t.Helper() |
| dir := t.TempDir() |
| |
| configBytes, err := yaml.Marshal(cfg) |
| if err != nil { |
| t.Fatalf("Could not marshal test injection config: %v", err) |
| } |
| _, values, _ := readInjectionSettings(t, "default") |
| var ( |
| configFile = filepath.Join(dir, "config-file.yaml") |
| valuesFile = filepath.Join(dir, "values-file.yaml") |
| port = 0 |
| ) |
| |
| if err := os.WriteFile(configFile, configBytes, 0o644); err != nil { // nolint: vetshadow |
| t.Fatalf("WriteFile(%v) failed: %v", configFile, err) |
| } |
| |
| if err := os.WriteFile(valuesFile, []byte(values.raw), 0o644); err != nil { // nolint: vetshadow |
| t.Fatalf("WriteFile(%v) failed: %v", valuesFile, err) |
| } |
| |
| // mesh config |
| m := mesh.DefaultMeshConfig() |
| store := model.NewFakeStore() |
| for i := 0; i < pcResources; i++ { |
| store.Create(newProxyConfig(fmt.Sprintf("pc-%d", i), "dubbo-system", &v1beta12.ProxyConfig{ |
| Concurrency: &wrapperspb.Int32Value{Value: int32(i % 5)}, |
| EnvironmentVariables: map[string]string{ |
| fmt.Sprintf("VAR_%d", i): fmt.Sprint(i), |
| }, |
| })) |
| } |
| pcs, _ := model.GetProxyConfigs(store, m) |
| env := model.Environment{ |
| Watcher: mesh.NewFixedWatcher(m), |
| PushContext: &model.PushContext{ |
| ProxyConfigs: pcs, |
| }, |
| ConfigStore: model.MakeIstioStore(store), |
| } |
| watcher, err := NewFileWatcher(configFile, valuesFile) |
| if err != nil { |
| t.Fatalf("NewFileWatcher() failed: %v", err) |
| } |
| wh, err := NewWebhook(WebhookParameters{ |
| Watcher: watcher, |
| Port: port, |
| Env: &env, |
| Mux: http.NewServeMux(), |
| }) |
| if err != nil { |
| t.Fatalf("NewWebhook() failed: %v", err) |
| } |
| return wh |
| } |
| |
| func TestRunAndServe(t *testing.T) { |
| // TODO: adjust the test to match prod defaults instead of fake defaults. |
| wh := createWebhook(t, minimalSidecarTemplate, 0) |
| stop := make(chan struct{}) |
| defer func() { close(stop) }() |
| wh.Run(stop) |
| |
| validReview := makeTestData(t, false, "v1beta1") |
| validReviewV1 := makeTestData(t, false, "v1") |
| skipReview := makeTestData(t, true, "v1beta1") |
| |
| // nolint: lll |
| validPatch := []byte(`[ |
| { |
| "op": "add", |
| "path": "/metadata/annotations", |
| "value": { |
| "prometheus.io/path": "/stats/prometheus", |
| "prometheus.io/port": "15020", |
| "prometheus.io/scrape": "true", |
| "sidecar.istio.io/status": "{\"initContainers\":[\"istio-init\"],\"containers\":[\"istio-proxy\"],\"volumes\":[\"istio-envoy\"],\"imagePullSecrets\":[\"istio-image-pull-secrets\"],\"revision\":\"default\"}" |
| } |
| }, |
| { |
| "op": "add", |
| "path": "/spec/volumes/1", |
| "value": { |
| "name": "v0" |
| } |
| }, |
| { |
| "op": "replace", |
| "path": "/spec/volumes/0/name", |
| "value": "istio-envoy" |
| }, |
| { |
| "op": "add", |
| "path": "/spec/initContainers/1", |
| "value": { |
| "name": "istio-init", |
| "resources": {} |
| } |
| }, |
| { |
| "op": "add", |
| "path": "/spec/containers/1", |
| "value": { |
| "name": "istio-proxy", |
| "resources": {} |
| } |
| }, |
| { |
| "op": "add", |
| "path": "/spec/imagePullSecrets/1", |
| "value": { |
| "name": "p0" |
| } |
| }, |
| { |
| "op": "replace", |
| "path": "/spec/imagePullSecrets/0/name", |
| "value": "istio-image-pull-secrets" |
| } |
| ]`) |
| |
| cases := []struct { |
| name string |
| body []byte |
| contentType string |
| wantAllowed bool |
| wantStatusCode int |
| wantPatch []byte |
| }{ |
| { |
| name: "valid", |
| body: validReview, |
| contentType: "application/json", |
| wantAllowed: true, |
| wantStatusCode: http.StatusOK, |
| wantPatch: validPatch, |
| }, |
| { |
| name: "valid(v1 version)", |
| body: validReviewV1, |
| contentType: "application/json", |
| wantAllowed: true, |
| wantStatusCode: http.StatusOK, |
| wantPatch: validPatch, |
| }, |
| { |
| name: "skipped", |
| body: skipReview, |
| contentType: "application/json", |
| wantAllowed: true, |
| wantStatusCode: http.StatusOK, |
| }, |
| { |
| name: "wrong content-type", |
| body: validReview, |
| contentType: "application/yaml", |
| wantAllowed: false, |
| wantStatusCode: http.StatusUnsupportedMediaType, |
| }, |
| { |
| name: "bad content", |
| body: []byte{0, 1, 2, 3, 4, 5}, // random data |
| contentType: "application/json", |
| wantAllowed: false, |
| wantStatusCode: http.StatusOK, |
| }, |
| { |
| name: "missing body", |
| contentType: "application/json", |
| wantAllowed: false, |
| wantStatusCode: http.StatusBadRequest, |
| }, |
| } |
| |
| for i, c := range cases { |
| t.Run(fmt.Sprintf("[%d] %s", i, c.name), func(t *testing.T) { |
| req := httptest.NewRequest("POST", "http://sidecar-injector/inject", bytes.NewReader(c.body)) |
| req.Header.Add("Content-Type", c.contentType) |
| |
| w := httptest.NewRecorder() |
| wh.serveInject(w, req) |
| res := w.Result() |
| |
| if res.StatusCode != c.wantStatusCode { |
| t.Fatalf("wrong status code: \ngot %v \nwant %v", res.StatusCode, c.wantStatusCode) |
| } |
| |
| if res.StatusCode != http.StatusOK { |
| return |
| } |
| |
| gotBody, err := io.ReadAll(res.Body) |
| if err != nil { |
| t.Fatalf("could not read body: %v", err) |
| } |
| var gotReview v1beta1.AdmissionReview |
| if err := json.Unmarshal(gotBody, &gotReview); err != nil { |
| t.Fatalf("could not decode response body: %v", err) |
| } |
| if gotReview.Response.Allowed != c.wantAllowed { |
| t.Fatalf("AdmissionReview.Response.Allowed is wrong : got %v want %v", |
| gotReview.Response.Allowed, c.wantAllowed) |
| } |
| |
| var gotPatch bytes.Buffer |
| if len(gotReview.Response.Patch) > 0 { |
| if err := json.Compact(&gotPatch, gotReview.Response.Patch); err != nil { |
| t.Fatalf(err.Error()) |
| } |
| } |
| var wantPatch bytes.Buffer |
| if len(c.wantPatch) > 0 { |
| if err := json.Compact(&wantPatch, c.wantPatch); err != nil { |
| t.Fatalf(err.Error()) |
| } |
| } |
| |
| if !bytes.Equal(gotPatch.Bytes(), wantPatch.Bytes()) { |
| t.Fatalf("got bad patch: \n got %v \n want %v", gotPatch.String(), wantPatch.String()) |
| } |
| }) |
| } |
| // Now Validate that metrics are created. |
| testSideCarInjectorMetrics(t) |
| } |
| |
| func testSideCarInjectorMetrics(t *testing.T) { |
| expected := []string{ |
| "sidecar_injection_requests_total", |
| "sidecar_injection_success_total", |
| "sidecar_injection_skip_total", |
| "sidecar_injection_failure_total", |
| } |
| for _, e := range expected { |
| retry.UntilSuccessOrFail(t, func() error { |
| got, err := sutil.GetMetricsCounterValueWithTags(e, nil) |
| if err != nil { |
| return err |
| } |
| if got <= 0 { |
| return fmt.Errorf("metric empty") |
| } |
| return nil |
| }) |
| } |
| } |
| |
| func benchmarkInjectServe(pcs int, b *testing.B) { |
| sidecarTemplate, _, _ := readInjectionSettings(b, "default") |
| wh := createWebhook(b, sidecarTemplate, pcs) |
| |
| stop := make(chan struct{}) |
| defer func() { close(stop) }() |
| wh.Run(stop) |
| |
| body := makeTestData(b, false, "v1beta1") |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| req := httptest.NewRequest("POST", "http://sidecar-injector/inject", bytes.NewReader(body)) |
| req.Header.Add("Content-Type", "application/json") |
| |
| wh.serveInject(httptest.NewRecorder(), req) |
| } |
| } |
| |
| func BenchmarkInjectServePC0(b *testing.B) { |
| benchmarkInjectServe(0, b) |
| } |
| |
| func BenchmarkInjectServePC5(b *testing.B) { |
| benchmarkInjectServe(5, b) |
| } |
| |
| func BenchmarkInjectServePC15(b *testing.B) { |
| benchmarkInjectServe(15, b) |
| } |
| |
| func TestEnablePrometheusAggregation(t *testing.T) { |
| tests := []struct { |
| name string |
| mesh *meshconfig.MeshConfig |
| anno map[string]string |
| want bool |
| }{ |
| { |
| "no settings", |
| nil, |
| nil, |
| true, |
| }, |
| { |
| "mesh on", |
| &meshconfig.MeshConfig{EnablePrometheusMerge: &wrapperspb.BoolValue{Value: true}}, |
| nil, |
| true, |
| }, |
| { |
| "mesh off", |
| &meshconfig.MeshConfig{EnablePrometheusMerge: &wrapperspb.BoolValue{Value: false}}, |
| nil, |
| false, |
| }, |
| { |
| "annotation on", |
| &meshconfig.MeshConfig{EnablePrometheusMerge: &wrapperspb.BoolValue{Value: false}}, |
| map[string]string{annotation.PrometheusMergeMetrics.Name: "true"}, |
| true, |
| }, |
| { |
| "annotation off", |
| &meshconfig.MeshConfig{EnablePrometheusMerge: &wrapperspb.BoolValue{Value: true}}, |
| map[string]string{annotation.PrometheusMergeMetrics.Name: "false"}, |
| false, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := enablePrometheusMerge(tt.mesh, tt.anno); got != tt.want { |
| t.Errorf("enablePrometheusMerge() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestParseInjectEnvs(t *testing.T) { |
| cases := []struct { |
| name string |
| in string |
| want map[string]string |
| }{ |
| { |
| name: "empty", |
| in: "/", |
| want: map[string]string{}, |
| }, |
| { |
| name: "no-kv", |
| in: "/inject", |
| want: map[string]string{}, |
| }, |
| { |
| name: "no-kv-with-tail", |
| in: "/inject/", |
| want: map[string]string{}, |
| }, |
| { |
| name: "one-kv", |
| in: "/inject/cluster/cluster1", |
| want: map[string]string{"ISTIO_META_CLUSTER_ID": "cluster1"}, |
| }, |
| { |
| name: "two-kv", |
| in: "/inject/cluster/cluster1/net/network1/", |
| want: map[string]string{"ISTIO_META_CLUSTER_ID": "cluster1", "ISTIO_META_NETWORK": "network1"}, |
| }, |
| { |
| name: "not-predefined-kv", |
| in: "/inject/cluster/cluster1/custom_env/foo", |
| want: map[string]string{"ISTIO_META_CLUSTER_ID": "cluster1", "CUSTOM_ENV": "foo"}, |
| }, |
| { |
| name: "one-key-without-value", |
| in: "/inject/cluster", |
| want: map[string]string{}, |
| }, |
| { |
| name: "key-without-value", |
| in: "/inject/cluster/cluster1/network", |
| want: map[string]string{"ISTIO_META_CLUSTER_ID": "cluster1"}, |
| }, |
| { |
| name: "key-with-values-contain-slashes", |
| in: "/inject/:ENV:cluster=cluster2:ENV:rootpage=/foo/bar", |
| want: map[string]string{"ISTIO_META_CLUSTER_ID": "cluster2", "ROOTPAGE": "/foo/bar"}, |
| }, |
| { |
| // this is to test the path not following :ENV: format, the |
| // path will be considered using slash as separator |
| name: "no-predefined-kv-with-values-contain-ENV-separator", |
| in: "/inject/rootpage1/value1/rootpage2/:ENV:abcd=efgh", |
| want: map[string]string{"ROOTPAGE1": "value1", "ROOTPAGE2": ":ENV:abcd=efgh"}, |
| }, |
| { |
| // this is to test the path following :ENV: format, but two variables |
| // do not have correct format, thus they will be ignored. Eg. :ENV:=abb |
| // :ENV:=, these two are not correct variables. |
| name: "no-predefined-kv-with-values-contain-ENV-separator-invalid-format", |
| in: "/inject/:ENV:rootpage1=efgh:ENV:=abb:ENV:=", |
| want: map[string]string{"ROOTPAGE1": "efgh"}, |
| }, |
| { |
| // this is to test that the path is an url encoded string, still using |
| // slash as separators |
| name: "no-predefined-kv-with-mixed-values", |
| in: func() string { |
| req, _ := http.NewRequest("GET", |
| "%2Finject%2Frootpage1%2Ffoo%2Frootpage2%2Fbar", nil) |
| return req.URL.Path |
| }(), |
| want: map[string]string{"ROOTPAGE1": "foo", "ROOTPAGE2": "bar"}, |
| }, |
| { |
| // this is to test that the path is an url encoded string and :ENV: as separator |
| // eg. /inject/:ENV:rootpage1=/foo/bar:ENV:rootpage2=/bar/toe but url encoded. |
| name: "no-predefined-kv-with-slashes", |
| in: func() string { |
| req, _ := http.NewRequest("GET", |
| "%2Finject%2F%3AENV%3Arootpage1%3D%2Ffoo%2Fbar%3AENV%3Arootpage2%3D%2Fbar%2Ftoe", nil) |
| return req.URL.Path |
| }(), |
| want: map[string]string{"ROOTPAGE1": "/foo/bar", "ROOTPAGE2": "/bar/toe"}, |
| }, |
| } |
| |
| for _, tc := range cases { |
| t.Run(tc.name, func(t *testing.T) { |
| actual := parseInjectEnvs(tc.in) |
| if !reflect.DeepEqual(actual, tc.want) { |
| t.Fatalf("Expected result %#v, but got %#v", tc.want, actual) |
| } |
| }) |
| } |
| } |
| |
| func newProxyConfig(name, ns string, spec config.Spec) config.Config { |
| return config.Config{ |
| Meta: config.Meta{ |
| GroupVersionKind: gvk.ProxyConfig, |
| Name: name, |
| Namespace: ns, |
| }, |
| Spec: spec, |
| } |
| } |
| |
| // defaultInstallPackageDir returns a path to a snapshot of the helm charts used for testing. |
| func defaultInstallPackageDir() string { |
| wd, err := os.Getwd() |
| if err != nil { |
| panic(err) |
| } |
| return filepath.Join(wd, "../../../manifests/") |
| } |