Skip to content

Commit ba21c6c

Browse files
authored
feat: Add support for Istio multicluster (argoproj#1274)
Signed-off-by: Shakti <[email protected]>
1 parent 503c520 commit ba21c6c

File tree

9 files changed

+275
-9
lines changed

9 files changed

+275
-9
lines changed

USERS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ Organizations below are **officially** using Argo Rollouts. Please send a PR wit
1919
1. [PayPal](https://www.paypal.com/)
2020
1. [Shipt](https://www.shipt.com/)
2121
1. [Nitro](https://www.gonitro.com)
22+
1. [Salesforce](https://www.salesforce.com/)

cmd/rollouts-controller/main.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"github.com/argoproj/argo-rollouts/rollout/trafficrouting/smi"
3030
controllerutil "github.com/argoproj/argo-rollouts/utils/controller"
3131
"github.com/argoproj/argo-rollouts/utils/defaults"
32-
"github.com/argoproj/argo-rollouts/utils/istio"
3332
istioutil "github.com/argoproj/argo-rollouts/utils/istio"
3433
logutil "github.com/argoproj/argo-rollouts/utils/log"
3534
"github.com/argoproj/argo-rollouts/utils/tolerantinformer"
@@ -84,7 +83,7 @@ func newCommand() *cobra.Command {
8483
stopCh := signals.SetupSignalHandler()
8584

8685
alb.SetDefaultVerifyWeight(albVerifyWeight)
87-
istio.SetIstioAPIVersion(istioVersion)
86+
istioutil.SetIstioAPIVersion(istioVersion)
8887
ambassador.SetAPIVersion(ambassadorVersion)
8988
smi.SetSMIAPIVersion(trafficSplitVersion)
9089

@@ -97,8 +96,6 @@ func newCommand() *cobra.Command {
9796
namespace = configNS
9897
log.Infof("Using namespace %s", namespace)
9998
}
100-
k8sRequestProvider := &metrics.K8sRequestsCountProvider{}
101-
kubeclientmetrics.AddMetricsTransportWrapper(config, k8sRequestProvider.IncKubernetesRequest)
10299

103100
kubeClient, err := kubernetes.NewForConfig(config)
104101
checkError(err)
@@ -134,14 +131,22 @@ func newCommand() *cobra.Command {
134131
// a single namespace (i.e. rollouts-controller --namespace foo).
135132
clusterDynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, resyncDuration, metav1.NamespaceAll, instanceIDTweakListFunc)
136133
// 3. We finally need an istio dynamic informer factory which does not use a tweakListFunc.
137-
istioDynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, resyncDuration, namespace, nil)
134+
_, istioPrimaryDynamicClient := istioutil.GetPrimaryClusterDynamicClient(kubeClient, namespace)
135+
if istioPrimaryDynamicClient == nil {
136+
istioPrimaryDynamicClient = dynamicClient
137+
}
138+
istioDynamicInformerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(istioPrimaryDynamicClient, resyncDuration, namespace, nil)
138139

139140
controllerNamespaceInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(
140141
kubeClient,
141142
resyncDuration,
142143
kubeinformers.WithNamespace(defaults.Namespace()))
143144
configMapInformer := controllerNamespaceInformerFactory.Core().V1().ConfigMaps()
144145
secretInformer := controllerNamespaceInformerFactory.Core().V1().Secrets()
146+
147+
k8sRequestProvider := &metrics.K8sRequestsCountProvider{}
148+
kubeclientmetrics.AddMetricsTransportWrapper(config, k8sRequestProvider.IncKubernetesRequest)
149+
145150
cm := controller.NewManager(
146151
namespace,
147152
kubeClient,
@@ -158,6 +163,7 @@ func newCommand() *cobra.Command {
158163
tolerantinformer.NewTolerantAnalysisRunInformer(dynamicInformerFactory),
159164
tolerantinformer.NewTolerantAnalysisTemplateInformer(dynamicInformerFactory),
160165
tolerantinformer.NewTolerantClusterAnalysisTemplateInformer(clusterDynamicInformerFactory),
166+
istioPrimaryDynamicClient,
161167
istioDynamicInformerFactory.ForResource(istioutil.GetIstioVirtualServiceGVR()).Informer(),
162168
istioDynamicInformerFactory.ForResource(istioutil.GetIstioDestinationRuleGVR()).Informer(),
163169
configMapInformer,
@@ -179,7 +185,7 @@ func newCommand() *cobra.Command {
179185
jobInformerFactory.Start(stopCh)
180186

181187
// Check if Istio installed on cluster before starting dynamicInformerFactory
182-
if istioutil.DoesIstioExist(dynamicClient, namespace) {
188+
if istioutil.DoesIstioExist(istioPrimaryDynamicClient, namespace) {
183189
istioDynamicInformerFactory.Start(stopCh)
184190
}
185191

controller/controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func NewManager(
114114
analysisRunInformer informers.AnalysisRunInformer,
115115
analysisTemplateInformer informers.AnalysisTemplateInformer,
116116
clusterAnalysisTemplateInformer informers.ClusterAnalysisTemplateInformer,
117+
istioPrimaryDynamicClient dynamic.Interface,
117118
istioVirtualServiceInformer cache.SharedIndexInformer,
118119
istioDestinationRuleInformer cache.SharedIndexInformer,
119120
configMapInformer coreinformers.ConfigMapInformer,
@@ -175,6 +176,7 @@ func NewManager(
175176
AnalysisRunInformer: analysisRunInformer,
176177
AnalysisTemplateInformer: analysisTemplateInformer,
177178
ClusterAnalysisTemplateInformer: clusterAnalysisTemplateInformer,
179+
IstioPrimaryDynamicClient: istioPrimaryDynamicClient,
178180
IstioVirtualServiceInformer: istioVirtualServiceInformer,
179181
IstioDestinationRuleInformer: istioDestinationRuleInformer,
180182
ReplicaSetInformer: replicaSetInformer,

docs/features/traffic-management/istio.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,72 @@ During the lifecycle of a Rollout using Istio DestinationRule, Argo Rollouts wil
246246
label of the canary and stable ReplicaSets
247247

248248

249+
## Multicluster Setup
250+
If you have [Istio multicluster setup](https://istio.io/latest/docs/setup/install/multicluster/)
251+
where the primary Istio cluster is different than the cluster where the Argo Rollout controller
252+
is running, then you need to do the following setup:
253+
254+
1. Create a `ServiceAccount` in the Istio primary cluster.
255+
```yaml
256+
apiVersion: v1
257+
kind: ServiceAccount
258+
metadata:
259+
name: argo-rollouts-istio-primary
260+
namespace: <any-namespace-preferrably-config-namespace>
261+
```
262+
2. Create a `ClusterRole` that provides access to Rollout controller in the Istio primary cluster.
263+
```yaml
264+
apiVersion: rbac.authorization.k8s.io/v1
265+
kind: ClusterRole
266+
metadata:
267+
name: argo-rollouts-istio-primary
268+
rules:
269+
- apiGroups:
270+
- networking.istio.io
271+
resources:
272+
- virtualservices
273+
- destinationrules
274+
verbs:
275+
- get
276+
- list
277+
- watch
278+
- update
279+
- patch
280+
```
281+
Note: If Argo Rollout controller is also installed in the Istio primary cluster, then you can reuse the
282+
`argo-rollouts-clusterrole` ClusterRole instead of creating a new one.
283+
3. Link the `ClusterRole` with the `ServiceAccount` in the Istio primary cluster.
284+
```yaml
285+
apiVersion: rbac.authorization.k8s.io/v1
286+
kind: ClusterRoleBinding
287+
metadata:
288+
name: argo-rollouts-istio-primary
289+
roleRef:
290+
apiGroup: rbac.authorization.k8s.io
291+
kind: ClusterRole
292+
name: argo-rollouts-istio-primary
293+
subjects:
294+
- kind: ServiceAccount
295+
name: argo-rollouts-istio-primary
296+
namespace: <namespace-of-the-service-account>
297+
```
298+
4. Now, use the following command to generate a secret for Rollout controller to access the Istio primary cluster.
299+
This secret will be applied to the cluster where Argo Rollout is running (i.e, Istio remote cluster),
300+
but will be generated from the Istio primary cluster. This secret can be generated right after Step 1,
301+
it only requires `ServiceAccount` to exist.
302+
[Reference to the command](https://istio.io/latest/docs/reference/commands/istioctl/#istioctl-experimental-create-remote-secret).
303+
```shell
304+
istioctl x create-remote-secret --type remote --name <cluster-name> \
305+
--namespace <namespace-of-the-service-account> \
306+
--service-account <service-account-created-in-step1> \
307+
--context="<ISTIO_PRIMARY_CLUSTER>" | \
308+
kubectl apply -f - --context="<ARGO_ROLLOUT_CLUSTER/ISTIO_REMOTE_CLUSTER>"
309+
```
310+
5. Label the secret.
311+
```shell
312+
kubectl label secret <istio-remote-secret> istio.argoproj.io/primary-cluster="true" -n <namespace-of-the-secret>
313+
```
314+
249315
## Comparison Between Approaches
250316

251317
There are some advantages and disadvantages of host-level traffic splitting vs. subset-level traffic

rollout/controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ type ControllerConfig struct {
9696
ServicesInformer coreinformers.ServiceInformer
9797
IngressInformer extensionsinformers.IngressInformer
9898
RolloutsInformer informers.RolloutInformer
99+
IstioPrimaryDynamicClient dynamic.Interface
99100
IstioVirtualServiceInformer cache.SharedIndexInformer
100101
IstioDestinationRuleInformer cache.SharedIndexInformer
101102
ResyncPeriod time.Duration
@@ -203,7 +204,7 @@ func NewController(cfg ControllerConfig) *Controller {
203204

204205
controller.IstioController = istio.NewIstioController(istio.IstioControllerConfig{
205206
ArgoprojClientSet: cfg.ArgoProjClientset,
206-
DynamicClientSet: cfg.DynamicClientSet,
207+
DynamicClientSet: cfg.IstioPrimaryDynamicClient,
207208
EnqueueRollout: controller.enqueueRollout,
208209
RolloutsInformer: cfg.RolloutsInformer,
209210
VirtualServiceInformer: cfg.IstioVirtualServiceInformer,

rollout/controller_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ func (f *fixture) newController(resync resyncFunc) (*Controller, informers.Share
511511
ServicesInformer: k8sI.Core().V1().Services(),
512512
IngressInformer: k8sI.Extensions().V1beta1().Ingresses(),
513513
RolloutsInformer: i.Argoproj().V1alpha1().Rollouts(),
514+
IstioPrimaryDynamicClient: dynamicClient,
514515
IstioVirtualServiceInformer: istioVirtualServiceInformer,
515516
IstioDestinationRuleInformer: istioDestinationRuleInformer,
516517
ResyncPeriod: resync(),

rollout/trafficrouting.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ func (c *Controller) NewTrafficRoutingReconciler(roCtx *rolloutContext) (Traffic
3434
}
3535
if rollout.Spec.Strategy.Canary.TrafficRouting.Istio != nil {
3636
if c.IstioController.VirtualServiceInformer.HasSynced() {
37-
return istio.NewReconciler(rollout, c.dynamicclientset, c.recorder, c.IstioController.VirtualServiceLister, c.IstioController.DestinationRuleLister), nil
37+
return istio.NewReconciler(rollout, c.IstioController.DynamicClientSet, c.recorder, c.IstioController.VirtualServiceLister, c.IstioController.DestinationRuleLister), nil
3838
} else {
39-
return istio.NewReconciler(rollout, c.dynamicclientset, c.recorder, nil, nil), nil
39+
return istio.NewReconciler(rollout, c.IstioController.DynamicClientSet, c.recorder, nil, nil), nil
4040
}
4141
}
4242
if rollout.Spec.Strategy.Canary.TrafficRouting.Nginx != nil {

utils/istio/multicluster.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package istio
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
log "github.com/sirupsen/logrus"
9+
corev1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/labels"
12+
"k8s.io/apimachinery/pkg/selection"
13+
"k8s.io/client-go/dynamic"
14+
"k8s.io/client-go/kubernetes"
15+
"k8s.io/client-go/tools/clientcmd"
16+
)
17+
18+
const (
19+
PrimaryClusterSecretLabel = "istio.argoproj.io/primary-cluster"
20+
)
21+
22+
func GetPrimaryClusterDynamicClient(kubeClient kubernetes.Interface, namespace string) (string, dynamic.Interface) {
23+
primaryClusterSecret := getPrimaryClusterSecret(kubeClient, namespace)
24+
if primaryClusterSecret != nil {
25+
clusterId, clientConfig, err := getKubeClientConfig(primaryClusterSecret)
26+
if err != nil {
27+
return clusterId, nil
28+
}
29+
30+
config, err := clientConfig.ClientConfig()
31+
if err != nil {
32+
log.Errorf("Error fetching primary ClientConfig: %v", err)
33+
return clusterId, nil
34+
}
35+
36+
dynamicClient, err := dynamic.NewForConfig(config)
37+
if err != nil {
38+
log.Errorf("Error building dynamic client from config: %v", err)
39+
return clusterId, nil
40+
}
41+
42+
return clusterId, dynamicClient
43+
}
44+
45+
return "", nil
46+
}
47+
48+
func getPrimaryClusterSecret(kubeClient kubernetes.Interface, namespace string) *corev1.Secret {
49+
req, err := labels.NewRequirement(PrimaryClusterSecretLabel, selection.Equals, []string{"true"})
50+
if err != nil {
51+
return nil
52+
}
53+
54+
secrets, err := kubeClient.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{Limit: 1, LabelSelector: req.String()})
55+
if err != nil {
56+
return nil
57+
}
58+
59+
if secrets != nil && len(secrets.Items) > 0 {
60+
return &secrets.Items[0]
61+
}
62+
63+
return nil
64+
}
65+
66+
func getKubeClientConfig(secret *corev1.Secret) (string, clientcmd.ClientConfig, error) {
67+
for clusterId, kubeConfig := range secret.Data {
68+
primaryClusterConfig, err := buildKubeClientConfig(kubeConfig)
69+
if err != nil {
70+
log.Errorf("Error building kubeconfig for primary cluster %s: %v", clusterId, err)
71+
return clusterId, nil, fmt.Errorf("error building primary cluster client %s: %v", clusterId, err)
72+
}
73+
log.Infof("Istio primary/config cluster is %s", clusterId)
74+
return clusterId, primaryClusterConfig, err
75+
}
76+
return "", nil, nil
77+
}
78+
79+
func buildKubeClientConfig(kubeConfig []byte) (clientcmd.ClientConfig, error) {
80+
if len(kubeConfig) == 0 {
81+
return nil, errors.New("kubeconfig is empty")
82+
}
83+
84+
rawConfig, err := clientcmd.Load(kubeConfig)
85+
if err != nil {
86+
return nil, fmt.Errorf("kubeconfig cannot be loaded: %v", err)
87+
}
88+
89+
if err := clientcmd.Validate(*rawConfig); err != nil {
90+
return nil, fmt.Errorf("kubeconfig is not valid: %v", err)
91+
}
92+
93+
clientConfig := clientcmd.NewDefaultClientConfig(*rawConfig, &clientcmd.ConfigOverrides{})
94+
return clientConfig, nil
95+
}

utils/istio/multicluster_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package istio
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
v1 "k8s.io/api/core/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/client-go/kubernetes/fake"
11+
)
12+
13+
var (
14+
secret0 = makeSecret("secret0", "namespace0", "primary0", []byte("kubeconfig0-0"))
15+
secret1 = makeSecret("secret1", "namespace1", "primary1", []byte("kubeconfig1-1"))
16+
rolloutControllerNamespace = "argo-rollout-ns"
17+
)
18+
19+
func TestGetPrimaryClusterDynamicClient(t *testing.T) {
20+
testCases := []struct {
21+
name string
22+
namespace string
23+
existingSecrets []*v1.Secret
24+
expectedClusterId string
25+
}{
26+
{
27+
"TestNoPrimaryClusterSecret",
28+
metav1.NamespaceAll,
29+
nil,
30+
"",
31+
},
32+
{
33+
"TestPrimaryClusterSingleSecret",
34+
metav1.NamespaceAll,
35+
[]*v1.Secret{
36+
secret0,
37+
},
38+
"primary0",
39+
},
40+
{
41+
"TestPrimaryClusterMultipleSecrets",
42+
metav1.NamespaceAll,
43+
[]*v1.Secret{
44+
secret0,
45+
secret1,
46+
},
47+
"primary0",
48+
},
49+
{
50+
"TestPrimaryClusterNoSecretInNamespaceForNamespacedController",
51+
rolloutControllerNamespace,
52+
[]*v1.Secret{
53+
secret0,
54+
},
55+
"",
56+
},
57+
{
58+
"TestPrimaryClusterSingleSecretInNamespaceForNamespacedController",
59+
rolloutControllerNamespace,
60+
[]*v1.Secret{
61+
makeSecret("secret0", rolloutControllerNamespace, "primary0", []byte("kubeconfig0-0")),
62+
},
63+
"primary0",
64+
},
65+
}
66+
67+
for _, tc := range testCases {
68+
t.Run(tc.name, func(t *testing.T) {
69+
var existingObjs []runtime.Object
70+
for _, s := range tc.existingSecrets {
71+
existingObjs = append(existingObjs, s)
72+
}
73+
74+
client := fake.NewSimpleClientset(existingObjs...)
75+
clusterId, _ := GetPrimaryClusterDynamicClient(client, tc.namespace)
76+
assert.Equal(t, tc.expectedClusterId, clusterId)
77+
})
78+
}
79+
}
80+
81+
func makeSecret(secret, namespace, clusterID string, kubeconfig []byte) *v1.Secret {
82+
return &v1.Secret{
83+
ObjectMeta: metav1.ObjectMeta{
84+
Name: secret,
85+
Namespace: namespace,
86+
Labels: map[string]string{
87+
PrimaryClusterSecretLabel: "true",
88+
},
89+
},
90+
Data: map[string][]byte{
91+
clusterID: kubeconfig,
92+
},
93+
}
94+
}

0 commit comments

Comments
 (0)