Skip to content

Commit f5b5d81

Browse files
authored
Implement ignoring istio virtual service on primary rollout stage (#6258)
Signed-off-by: Yoshiki Fujikane <[email protected]>
1 parent 96c15d8 commit f5b5d81

File tree

6 files changed

+318
-3
lines changed

6 files changed

+318
-3
lines changed

pkg/app/pipedv1/plugin/kubernetes/deployment/primary.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,30 @@ func (p *Plugin) executeK8sPrimaryRolloutStage(ctx context.Context, input *sdk.E
7474
case kubeconfig.KubernetesTrafficRoutingMethodPodSelector:
7575
primaryManifests = manifests
7676
case kubeconfig.KubernetesTrafficRoutingMethodIstio:
77-
// TODO: support routing by Istio
78-
lp.Errorf("Traffic routing method %v is not yet implemented", routingMethod)
79-
return sdk.StageStatusFailure
77+
// In case of routing by Istio,
78+
// VirtualService manifest will be used to manipulate the traffic ratio.
79+
// Other manifests can be used as primary manifests.
80+
// Firstly, find the VirtualService manifests.
81+
istioCfg := appCfg.TrafficRouting.Istio
82+
if istioCfg == nil {
83+
istioCfg = &kubeconfig.IstioTrafficRouting{}
84+
}
85+
trafficRoutingManifests, err := findIstioVirtualServiceManifests(manifests, istioCfg.VirtualService)
86+
if err != nil {
87+
lp.Errorf("Failed while finding traffic routing manifest: (%v)", err)
88+
return sdk.StageStatusFailure
89+
}
90+
// Then remove them from the list of primary manifests.
91+
// Prior the first one if there are some virtual services.
92+
if len(trafficRoutingManifests) > 0 {
93+
primaryManifests = make([]provider.Manifest, 0, len(manifests)-1)
94+
for _, m := range manifests {
95+
if m.Key() == trafficRoutingManifests[0].Key() {
96+
continue
97+
}
98+
primaryManifests = append(primaryManifests, m)
99+
}
100+
}
80101
default:
81102
lp.Errorf("Traffic routing method %v is not supported", routingMethod)
82103
return sdk.StageStatusFailure

pkg/app/pipedv1/plugin/kubernetes/deployment/primary_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,222 @@ func TestPlugin_executeK8sPrimaryRolloutStage(t *testing.T) {
9898
assert.Equal(t, "simple", service.GetName())
9999
}
100100

101+
func TestPlugin_executeK8sPrimaryRolloutStage_withIstio(t *testing.T) {
102+
t.Parallel()
103+
104+
ctx := t.Context()
105+
106+
// initialize tool registry
107+
testRegistry := toolregistrytest.NewTestToolRegistry(t)
108+
109+
// read the application config from the example file
110+
appCfg := sdk.LoadApplicationConfigForTest[kubeConfigPkg.KubernetesApplicationSpec](t, filepath.Join("testdata", "primary_rollout_istio", "app.pipecd.yaml"), "kubernetes")
111+
112+
// initialize deploy target config and dynamic client for assertions with envtest
113+
dtConfig, dynamicClient := setupTestDeployTargetConfigAndDynamicClient(t)
114+
115+
// install istio
116+
installIstioCRDs(t, dtConfig)
117+
118+
// execute k8s sync stage
119+
{
120+
input := &sdk.ExecuteStageInput[kubeConfigPkg.KubernetesApplicationSpec]{
121+
Request: sdk.ExecuteStageRequest[kubeConfigPkg.KubernetesApplicationSpec]{
122+
StageName: "K8S_SYNC",
123+
StageConfig: []byte(``),
124+
TargetDeploymentSource: sdk.DeploymentSource[kubeConfigPkg.KubernetesApplicationSpec]{
125+
ApplicationDirectory: filepath.Join("testdata", "primary_rollout_istio"),
126+
CommitHash: "0123456789",
127+
ApplicationConfig: appCfg,
128+
ApplicationConfigFilename: "app.pipecd.yaml",
129+
},
130+
Deployment: sdk.Deployment{
131+
PipedID: "piped-id",
132+
ApplicationID: "app-id",
133+
},
134+
},
135+
Client: sdk.NewClient(nil, "kubernetes", "", "", logpersistertest.NewTestLogPersister(t), testRegistry),
136+
Logger: zaptest.NewLogger(t),
137+
}
138+
139+
plugin := &Plugin{}
140+
141+
status := plugin.executeK8sSyncStage(ctx, input, []*sdk.DeployTarget[kubeConfigPkg.KubernetesDeployTargetConfig]{
142+
{
143+
Name: "default",
144+
Config: *dtConfig,
145+
},
146+
})
147+
148+
assert.Equal(t, sdk.StageStatusSuccess, status)
149+
150+
// check the existance of deployment, service, virutal service
151+
_, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
152+
assert.NoError(t, err)
153+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
154+
assert.NoError(t, err)
155+
verifyVirtualServiceRouting(t, dynamicClient, "traffic-test-vs", []expectedRoute{
156+
{
157+
host: "traffic-test",
158+
subset: "primary",
159+
weight: 100,
160+
},
161+
})
162+
}
163+
164+
// execute canary stage
165+
{
166+
input := &sdk.ExecuteStageInput[kubeConfigPkg.KubernetesApplicationSpec]{
167+
Request: sdk.ExecuteStageRequest[kubeConfigPkg.KubernetesApplicationSpec]{
168+
StageName: "K8S_CANARY_ROLLOUT",
169+
StageConfig: []byte(`{"replicas": "100%"}`),
170+
TargetDeploymentSource: sdk.DeploymentSource[kubeConfigPkg.KubernetesApplicationSpec]{
171+
ApplicationDirectory: filepath.Join("testdata", "primary_rollout_istio"),
172+
CommitHash: "0123456789",
173+
ApplicationConfig: appCfg,
174+
ApplicationConfigFilename: "app.pipecd.yaml",
175+
},
176+
Deployment: sdk.Deployment{
177+
PipedID: "piped-id",
178+
ApplicationID: "app-id",
179+
},
180+
},
181+
Client: sdk.NewClient(nil, "kubernetes", "", "", logpersistertest.NewTestLogPersister(t), testRegistry),
182+
Logger: zaptest.NewLogger(t),
183+
}
184+
185+
plugin := &Plugin{}
186+
187+
status := plugin.executeK8sCanaryRolloutStage(ctx, input, []*sdk.DeployTarget[kubeConfigPkg.KubernetesDeployTargetConfig]{
188+
{
189+
Name: "default",
190+
Config: *dtConfig,
191+
},
192+
})
193+
194+
assert.Equal(t, sdk.StageStatusSuccess, status)
195+
196+
// check the existance of deployment, service, virutal service
197+
_, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
198+
assert.NoError(t, err)
199+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
200+
assert.NoError(t, err)
201+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test-canary", metav1.GetOptions{})
202+
assert.NoError(t, err)
203+
verifyVirtualServiceRouting(t, dynamicClient, "traffic-test-vs", []expectedRoute{
204+
{
205+
host: "traffic-test",
206+
subset: "primary",
207+
weight: 100,
208+
},
209+
})
210+
}
211+
212+
// execute traffic routing stage
213+
{
214+
input := &sdk.ExecuteStageInput[kubeConfigPkg.KubernetesApplicationSpec]{
215+
Request: sdk.ExecuteStageRequest[kubeConfigPkg.KubernetesApplicationSpec]{
216+
StageName: "K8S_TRAFFIC_ROUTING",
217+
StageConfig: []byte(`{"canary": "30%"}`),
218+
TargetDeploymentSource: sdk.DeploymentSource[kubeConfigPkg.KubernetesApplicationSpec]{
219+
ApplicationDirectory: filepath.Join("testdata", "primary_rollout_istio"),
220+
CommitHash: "0123456789",
221+
ApplicationConfig: appCfg,
222+
ApplicationConfigFilename: "app.pipecd.yaml",
223+
},
224+
Deployment: sdk.Deployment{
225+
PipedID: "piped-id",
226+
ApplicationID: "app-id",
227+
},
228+
},
229+
Client: sdk.NewClient(nil, "kubernetes", "", "", logpersistertest.NewTestLogPersister(t), testRegistry),
230+
Logger: zaptest.NewLogger(t),
231+
}
232+
233+
plugin := &Plugin{}
234+
235+
status := plugin.executeK8sTrafficRoutingStage(ctx, input, []*sdk.DeployTarget[kubeConfigPkg.KubernetesDeployTargetConfig]{
236+
{
237+
Name: "default",
238+
Config: *dtConfig,
239+
},
240+
})
241+
242+
assert.Equal(t, sdk.StageStatusSuccess, status)
243+
244+
// check the existance of deployment, service, virutal service
245+
_, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
246+
assert.NoError(t, err)
247+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
248+
assert.NoError(t, err)
249+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test-canary", metav1.GetOptions{})
250+
assert.NoError(t, err)
251+
verifyVirtualServiceRouting(t, dynamicClient, "traffic-test-vs", []expectedRoute{
252+
{
253+
host: "traffic-test",
254+
subset: "primary",
255+
weight: 70,
256+
},
257+
{
258+
host: "traffic-test",
259+
subset: "canary",
260+
weight: 30,
261+
},
262+
})
263+
}
264+
265+
// execute primary stage
266+
input := &sdk.ExecuteStageInput[kubeConfigPkg.KubernetesApplicationSpec]{
267+
Request: sdk.ExecuteStageRequest[kubeConfigPkg.KubernetesApplicationSpec]{
268+
StageName: "K8S_PRIMARY_ROLLOUT",
269+
StageConfig: []byte(`{}`),
270+
TargetDeploymentSource: sdk.DeploymentSource[kubeConfigPkg.KubernetesApplicationSpec]{
271+
ApplicationDirectory: filepath.Join("testdata", "primary_rollout_istio"),
272+
CommitHash: "0123456789",
273+
ApplicationConfig: appCfg,
274+
ApplicationConfigFilename: "app.pipecd.yaml",
275+
},
276+
Deployment: sdk.Deployment{
277+
PipedID: "piped-id",
278+
ApplicationID: "app-id",
279+
},
280+
},
281+
Client: sdk.NewClient(nil, "kubernetes", "", "", logpersistertest.NewTestLogPersister(t), testRegistry),
282+
Logger: zaptest.NewLogger(t),
283+
}
284+
285+
plugin := &Plugin{}
286+
287+
status := plugin.executeK8sPrimaryRolloutStage(ctx, input, []*sdk.DeployTarget[kubeConfigPkg.KubernetesDeployTargetConfig]{
288+
{
289+
Name: "default",
290+
Config: *dtConfig,
291+
},
292+
})
293+
294+
assert.Equal(t, sdk.StageStatusSuccess, status)
295+
296+
// check the existance of deployment, service, virutal service
297+
_, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
298+
assert.NoError(t, err)
299+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}).Namespace("default").Get(ctx, "traffic-test", metav1.GetOptions{})
300+
assert.NoError(t, err)
301+
_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("default").Get(ctx, "traffic-test-canary", metav1.GetOptions{})
302+
assert.NoError(t, err)
303+
verifyVirtualServiceRouting(t, dynamicClient, "traffic-test-vs", []expectedRoute{
304+
{
305+
host: "traffic-test",
306+
subset: "primary",
307+
weight: 70,
308+
},
309+
{
310+
host: "traffic-test",
311+
subset: "canary",
312+
weight: 30,
313+
},
314+
})
315+
}
316+
101317
func TestPlugin_executeK8sPrimaryRolloutStage_withPrune(t *testing.T) {
102318
t.Parallel()
103319

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
apiVersion: pipecd.dev/v1beta1
2+
kind: Application
3+
spec:
4+
name: primary-rollout-istio-test
5+
description: Test data for primary rollout using Istio method
6+
plugins:
7+
kubernetes:
8+
input:
9+
manifests:
10+
- deployment.yaml
11+
- service.yaml
12+
- virtualservice.yaml
13+
kubectlVersion: 1.32.2
14+
service:
15+
name: traffic-test
16+
trafficRouting:
17+
method: istio
18+
istio:
19+
editableRoutes: []
20+
virtualService:
21+
name: traffic-test-vs
22+
pipeline:
23+
stages:
24+
- name: K8S_CANARY_ROLLOUT
25+
with:
26+
replicas: 100%
27+
- name: K8S_TRAFFIC_ROUTING
28+
with:
29+
canary: 30%
30+
- name: K8S_PRIMARY_ROLLOUT
31+
- name: K8S_TRAFFIC_ROUTING
32+
with:
33+
primary: 100%
34+
- name: K8S_CANARY_CLEAN
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: traffic-test
5+
spec:
6+
replicas: 3
7+
selector:
8+
matchLabels:
9+
app: traffic-test
10+
template:
11+
metadata:
12+
labels:
13+
app: traffic-test
14+
pipecd.dev/variant: primary
15+
spec:
16+
containers:
17+
- name: traffic-test
18+
image: nginx:1.22
19+
ports:
20+
- containerPort: 80
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: traffic-test
5+
spec:
6+
selector:
7+
app: traffic-test
8+
ports:
9+
- port: 80
10+
targetPort: 80
11+
type: ClusterIP
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: networking.istio.io/v1
2+
kind: VirtualService
3+
metadata:
4+
name: traffic-test-vs
5+
spec:
6+
hosts:
7+
- traffic-test
8+
http:
9+
- route:
10+
- destination:
11+
host: traffic-test
12+
subset: primary
13+
weight: 100

0 commit comments

Comments
 (0)