Repository: istio/istio (go)

Issue:
changes in delegate virtual service do not take effect
### Bug Description

Similar to #29123 seems to be happening with Istio 1.12.

Suppose we have following setup:

```
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: foo
  namespace: foo
spec:
  hosts:
  - bar.bar
  http:
  - match:
    - uri:
        prefix: "/prefix"
    delegate:
      name: bar
      namespace: bar
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: bar
  namespace: bar
spec:
  http:
  - route:
    - destination:
        host: bar.bar
        subset: stable
      weight: 90
    - destination:
        host: bar.bar
        subset: canary
      weight: 10
```

When we change the `bar` virtual service, for example, change the weight from 90,10 to 80,20, routes are not updated on the foo sidecar proxy. Restarting the pod does not get the routes updated either. Triggering some sort of update over the `foo` virtual service does update the routes.


### Version

```prose
$ istioctl version
client version: 1.12.1
control plane version: 1.12.1
data plane version: 1.12.1

$ kubectl version --short
Client Version: v1.21.1
Server Version: v1.21.2-eks-0389ca3
```


### Additional Information

From Istiod logs I can see that VirtualServices are indeed being merged, and an update pushed, but if I check a config dump of the proxy, the weights are not updated.

Pull request:
Bugfix: changes in delegate virtual service do not take effect
**Please provide a description of this PR:**
The root cause is that the rds cache is not cleared when delegate virtual services were updated

This pr adds delegate virtual services to dependent configs of the rds cache entry, so that we can clear the cache when delegate virtual services are updated

Fix #36525

Patch:
diff --git a/operator/cmd/mesh/operator-init.go b/operator/cmd/mesh/operator-init.go
index b37a8be4a2..205141ad28 100644
--- a/operator/cmd/mesh/operator-init.go
+++ b/operator/cmd/mesh/operator-init.go
@@ -127,7 +127,7 @@ func operatorInit(args *rootArgs, oiArgs *operatorInitArgs, l clog.Logger) {
 		}
 	}
 
-	if err := createNamespace(kubeClient, oiArgs.common.operatorNamespace, ""); err != nil {
+	if err := createNamespace(kubeClient, oiArgs.common.operatorNamespace, "", opts.DryRun); err != nil {
 		l.LogAndFatal(err)
 	}
 
@@ -138,7 +138,7 @@ func operatorInit(args *rootArgs, oiArgs *operatorInitArgs, l clog.Logger) {
 		namespaces = append(namespaces, istioNamespace)
 	}
 	for _, ns := range namespaces {
-		if err := createNamespace(kubeClient, ns, ""); err != nil {
+		if err := createNamespace(kubeClient, ns, "", opts.DryRun); err != nil {
 			l.LogAndFatal(err)
 		}
 	}
diff --git a/operator/cmd/mesh/shared.go b/operator/cmd/mesh/shared.go
index 85fcec9571..1f8c941fc4 100644
--- a/operator/cmd/mesh/shared.go
+++ b/operator/cmd/mesh/shared.go
@@ -181,8 +181,8 @@ func getCRAndNamespaceFromFile(filePath string, l clog.Logger) (customResource s
 }
 
 // createNamespace creates a namespace using the given k8s interface.
-func createNamespace(cs kubernetes.Interface, namespace string, network string) error {
-	return helmreconciler.CreateNamespace(cs, namespace, network)
+func createNamespace(cs kubernetes.Interface, namespace string, network string, dryRun bool) error {
+	return helmreconciler.CreateNamespace(cs, namespace, network, dryRun)
 }
 
 // saveIOPToCluster saves the state in an IOP CR in the cluster.
diff --git a/operator/pkg/helmreconciler/reconciler.go b/operator/pkg/helmreconciler/reconciler.go
index a58d499595..30416077e8 100644
--- a/operator/pkg/helmreconciler/reconciler.go
+++ b/operator/pkg/helmreconciler/reconciler.go
@@ -515,7 +515,10 @@ func (h *HelmReconciler) reportPrunedObjectKind() {
 }
 
 // CreateNamespace creates a namespace using the given k8s interface.
-func CreateNamespace(cs kubernetes.Interface, namespace string, network string) error {
+func CreateNamespace(cs kubernetes.Interface, namespace string, network string, dryRun bool) error {
+	if dryRun {
+		return nil
+	}
 	if namespace == "" {
 		// Setup default namespace
 		namespace = name.IstioDefaultNamespace
@@ -597,10 +600,7 @@ func (h *HelmReconciler) analyzeWebhooks(whs []string) error {
 
 // createNamespace creates a namespace using the given k8s client.
 func (h *HelmReconciler) createNamespace(namespace string, network string) error {
-	if h.opts.DryRun {
-		return nil
-	}
-	return CreateNamespace(h.kubeClient, namespace, network)
+	return CreateNamespace(h.kubeClient, namespace, network, h.opts.DryRun)
 }
 
 func (h *HelmReconciler) networkName() string {
diff --git a/pilot/pkg/bootstrap/istio_ca.go b/pilot/pkg/bootstrap/istio_ca.go
index 58fb3c37e5..9bc4f606ae 100644
--- a/pilot/pkg/bootstrap/istio_ca.go
+++ b/pilot/pkg/bootstrap/istio_ca.go
@@ -333,8 +333,8 @@ func (s *Server) handleCACertsFileWatch() {
 
 		case event, ok := <-s.cacertsWatcher.Events:
 			if !ok {
-				log.Info("Failed to catch events on cacerts files")
-				continue
+				log.Debug("plugin cacerts watch stopped")
+				return
 			}
 
 			if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
@@ -347,7 +347,11 @@ func (s *Server) handleCACertsFileWatch() {
 		case err := <-s.cacertsWatcher.Errors:
 			if err != nil {
 				log.Error("Failed to catch events on cacerts file: ", err)
+				return
 			}
+
+		case <-s.internalStop:
+			return
 		}
 	}
 }
diff --git a/pilot/pkg/model/push_context.go b/pilot/pkg/model/push_context.go
index 0b69e187f7..cf1b723541 100644
--- a/pilot/pkg/model/push_context.go
+++ b/pilot/pkg/model/push_context.go
@@ -842,6 +842,9 @@ func (ps *PushContext) VirtualServicesForGateway(proxy *Proxy, gateway string) [
 // DelegateVirtualServicesConfigKey lists all the delegate virtual services configkeys associated with the provided virtual services
 func (ps *PushContext) DelegateVirtualServicesConfigKey(vses []config.Config) []ConfigKey {
 	var out []ConfigKey
+	if !features.EnableVirtualServiceDelegate {
+		return out
+	}
 	for _, vs := range vses {
 		out = append(out, ps.virtualServiceIndex.delegates[ConfigKey{Kind: gvk.VirtualService, Namespace: vs.Namespace, Name: vs.Name}]...)
 	}
diff --git a/pilot/pkg/networking/core/v1alpha3/httproute.go b/pilot/pkg/networking/core/v1alpha3/httproute.go
index 51c5230c93..a2e7dd5cbd 100644
--- a/pilot/pkg/networking/core/v1alpha3/httproute.go
+++ b/pilot/pkg/networking/core/v1alpha3/httproute.go
@@ -288,16 +288,17 @@ func BuildSidecarOutboundVirtualHosts(node *model.Proxy, push *model.PushContext
 		})
 
 		routeCache = &istio_route.Cache{
-			RouteName:       routeName,
-			ProxyVersion:    node.Metadata.IstioVersion,
-			ClusterID:       string(node.Metadata.ClusterID),
-			DNSDomain:       node.DNSDomain,
-			DNSCapture:      bool(node.Metadata.DNSCapture),
-			DNSAutoAllocate: bool(node.Metadata.DNSAutoAllocate),
-			ListenerPort:    listenerPort,
-			Services:        services,
-			VirtualServices: virtualServices,
-			EnvoyFilterKeys: efKeys,
+			RouteName:               routeName,
+			ProxyVersion:            node.Metadata.IstioVersion,
+			ClusterID:               string(node.Metadata.ClusterID),
+			DNSDomain:               node.DNSDomain,
+			DNSCapture:              bool(node.Metadata.DNSCapture),
+			DNSAutoAllocate:         bool(node.Metadata.DNSAutoAllocate),
+			ListenerPort:            listenerPort,
+			Services:                services,
+			VirtualServices:         virtualServices,
+			DelegateVirtualServices: push.DelegateVirtualServicesConfigKey(virtualServices),
+			EnvoyFilterKeys:         efKeys,
 		}
 	}
 
diff --git a/pilot/pkg/networking/core/v1alpha3/route/route_cache.go b/pilot/pkg/networking/core/v1alpha3/route/route_cache.go
index 211d2f1f6c..c5dd486ee3 100644
--- a/pilot/pkg/networking/core/v1alpha3/route/route_cache.go
+++ b/pilot/pkg/networking/core/v1alpha3/route/route_cache.go
@@ -43,11 +43,12 @@ type Cache struct {
 	// This depends on DNSCapture.
 	DNSAutoAllocate bool
 
-	ListenerPort     int
-	Services         []*model.Service
-	VirtualServices  []config.Config
-	DestinationRules []*config.Config
-	EnvoyFilterKeys  []string
+	ListenerPort            int
+	Services                []*model.Service
+	VirtualServices         []config.Config
+	DelegateVirtualServices []model.ConfigKey
+	DestinationRules        []*config.Config
+	EnvoyFilterKeys         []string
 }
 
 func (r *Cache) Cacheable() bool {
@@ -74,13 +75,17 @@ func (r *Cache) Cacheable() bool {
 }
 
 func (r *Cache) DependentConfigs() []model.ConfigKey {
-	configs := make([]model.ConfigKey, len(r.Services)+len(r.VirtualServices)+len(r.DestinationRules))
+	configs := make([]model.ConfigKey, 0, len(r.Services)+len(r.VirtualServices)+
+		len(r.DelegateVirtualServices)+len(r.DestinationRules)+len(r.EnvoyFilterKeys))
 	for _, svc := range r.Services {
 		configs = append(configs, model.ConfigKey{Kind: gvk.ServiceEntry, Name: string(svc.Hostname), Namespace: svc.Attributes.Namespace})
 	}
 	for _, vs := range r.VirtualServices {
 		configs = append(configs, model.ConfigKey{Kind: gvk.VirtualService, Name: vs.Name, Namespace: vs.Namespace})
 	}
+	// add delegate virtual services to dependent configs
+	// so that we can clear the rds cache when delegate virtual services are updated
+	configs = append(configs, r.DelegateVirtualServices...)
 	for _, dr := range r.DestinationRules {
 		configs = append(configs, model.ConfigKey{Kind: gvk.DestinationRule, Name: dr.Name, Namespace: dr.Namespace})
 	}
diff --git a/releasenotes/notes/36570.yaml b/releasenotes/notes/36570.yaml
new file mode 100644
index 0000000000..721ec7ee15
--- /dev/null
+++ b/releasenotes/notes/36570.yaml
@@ -0,0 +1,6 @@
+apiVersion: release-notes/v2
+kind: bug-fix
+area: istioctl
+releaseNotes:
+  - |
+    **Fixed** `operator init --dry-run` creates unexpected namespaces.
diff --git a/releasenotes/notes/refresh-rds-cache.yaml b/releasenotes/notes/refresh-rds-cache.yaml
new file mode 100644
index 0000000000..c9cf83b00a
--- /dev/null
+++ b/releasenotes/notes/refresh-rds-cache.yaml
@@ -0,0 +1,8 @@
+apiVersion: release-notes/v2
+kind: bug-fix
+area: traffic-management
+issue:
+- https://github.com/istio/istio/issues/36525
+releaseNotes:
+- |
+  **Fixed** changes in delegate virtual service do not take effect when rds cache enabled


Test patch:
diff --git a/pilot/pkg/networking/core/v1alpha3/route/route_cache_test.go b/pilot/pkg/networking/core/v1alpha3/route/route_cache_test.go
new file mode 100644
index 0000000000..4921e63c9b
--- /dev/null
+++ b/pilot/pkg/networking/core/v1alpha3/route/route_cache_test.go
@@ -0,0 +1,83 @@
+// 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 route
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
+
+	networking "istio.io/api/networking/v1alpha3"
+	"istio.io/istio/pilot/pkg/model"
+	"istio.io/istio/pkg/config"
+	"istio.io/istio/pkg/config/schema/gvk"
+)
+
+func TestClearRDSCacheOnDelegateUpdate(t *testing.T) {
+	xdsCache := model.NewXdsCache()
+	// root virtual service
+	root := config.Config{
+		Meta: config.Meta{Name: "root", Namespace: "default"},
+		Spec: &networking.VirtualService{
+			Http: []*networking.HTTPRoute{
+				{
+					Name: "route",
+					Delegate: &networking.Delegate{
+						Namespace: "default",
+						Name:      "delegate",
+					},
+				},
+			},
+		},
+	}
+	// delegate virtual service
+	delegate := model.ConfigKey{Kind: gvk.VirtualService, Name: "delegate", Namespace: "default"}
+	// rds cache entry
+	entry := Cache{
+		VirtualServices:         []config.Config{root},
+		DelegateVirtualServices: []model.ConfigKey{delegate},
+		ListenerPort:            8080,
+	}
+	resource := &discovery.Resource{Name: "bar"}
+
+	// add resource to cache
+	xdsCache.Add(&entry, &model.PushRequest{Start: time.Now()}, resource)
+	if got, found := xdsCache.Get(&entry); !found || !reflect.DeepEqual(got, resource) {
+		t.Fatalf("rds cache was not updated")
+	}
+
+	// clear cache when delegate virtual service is updated
+	// this func is called by `dropCacheForRequest` in `initPushContext`
+	xdsCache.Clear(map[model.ConfigKey]struct{}{
+		delegate: {},
+	})
+	if _, found := xdsCache.Get(&entry); found {
+		t.Fatalf("rds cache was not cleared")
+	}
+
+	// add resource to cache
+	xdsCache.Add(&entry, &model.PushRequest{Start: time.Now()}, resource)
+	irrelevantDelegate := model.ConfigKey{Kind: gvk.VirtualService, Name: "foo", Namespace: "default"}
+
+	// don't clear cache when irrelevant delegate virtual service is updated
+	xdsCache.Clear(map[model.ConfigKey]struct{}{
+		irrelevantDelegate: {},
+	})
+	if got, found := xdsCache.Get(&entry); !found || !reflect.DeepEqual(got, resource) {
+		t.Fatalf("rds cache was cleared by irrelevant delegate virtual service update")
+	}
+}