Skip to content

Commit e9ffe80

Browse files
authored
feat: rollback mechanism virtualservice patch in model endpoint (#655)
# Description When creating/patching/deleting VirtualService for model endpoint related action, if there's any error happened after the action is successfully run, there's a possibility of mismatch state between the resource state in Kubernetes vs what is being recorded in database (as this will not be updated). # Modifications Changes: - Add `GetVirtualService` function to get the current state of VirtualService - Add `cleanVirtualServiceFields` function to remove not-needed field when creating or patching resource, e.g. UUID or generation number, if this isn't set to empty/default, the Patch/Create will not succeed - Flow, if there's any error occur after the create/patching/delete happened, rollback the changes in Kubernetes to previous state - Create -> remove the newly created VirtualService - Patch -> re-patch the VirtualService to previous state - Delete -> recreate the VirtualService if previously there's an existing one # Tests <!-- Besides the existing / updated automated tests, what specific scenarios should be tested? Consider the backward compatibility of the changes, whether corner cases are covered, etc. Please describe the tests and check the ones that have been completed. Eg: - [x] Deploying new and existing standard models - [ ] Deploying PyFunc models --> # Checklist - [x] Added PR label - [x] Added unit test, integration, and/or e2e tests - [x] Tested locally - [ ] Updated documentation - [ ] Update Swagger spec if the PR introduce API changes - [ ] Regenerated Golang and Python client if the PR introduces API changes # Release Notes ```release-note NONE ```
1 parent ade3fdc commit e9ffe80

File tree

6 files changed

+383
-13
lines changed

6 files changed

+383
-13
lines changed

api/istio/istio.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Config struct {
4141

4242
// Client interface.
4343
type Client interface {
44+
GetVirtualService(ctx context.Context, namespace, name string) (*istiov1beta1.VirtualService, error)
4445
CreateVirtualService(ctx context.Context, namespace string, vs *istiov1beta1.VirtualService) (*istiov1beta1.VirtualService, error)
4546
PatchVirtualService(ctx context.Context, namespace string, vs *istiov1beta1.VirtualService) (*istiov1beta1.VirtualService, error)
4647
DeleteVirtualService(ctx context.Context, namespace, name string) error
@@ -71,6 +72,10 @@ func newClient(networking networkingv1beta1.NetworkingV1beta1Interface) (*client
7172
}, nil
7273
}
7374

75+
func (c *client) GetVirtualService(ctx context.Context, namespace, name string) (*istiov1beta1.VirtualService, error) {
76+
return c.networking.VirtualServices(namespace).Get(ctx, name, metav1.GetOptions{})
77+
}
78+
7479
func (c *client) CreateVirtualService(ctx context.Context, namespace string, vs *istiov1beta1.VirtualService) (*istiov1beta1.VirtualService, error) {
7580
return c.networking.VirtualServices(namespace).Create(ctx, vs, metav1.CreateOptions{})
7681
}

api/istio/istio_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"reflect"
2020
"testing"
2121

22+
"github.com/pkg/errors"
2223
"github.com/stretchr/testify/assert"
2324
"github.com/stretchr/testify/mock"
2425
istionetv1beta1 "istio.io/api/networking/v1beta1"
@@ -76,6 +77,83 @@ var (
7677
}
7778
)
7879

80+
func Test_client_GetVirtualService(t *testing.T) {
81+
clientSet := istiofake.Clientset{}
82+
type fields struct {
83+
networking istiocliv1beta1.NetworkingV1beta1Interface
84+
}
85+
type args struct {
86+
ctx context.Context
87+
namespace string
88+
name string
89+
}
90+
91+
tests := []struct {
92+
name string
93+
fields fields
94+
mockFunc func(m istiocliv1beta1.NetworkingV1beta1Interface)
95+
args args
96+
want *istiov1beta1.VirtualService
97+
wantErr bool
98+
}{
99+
{
100+
name: "success",
101+
fields: fields{
102+
networking: clientSet.NetworkingV1beta1(),
103+
},
104+
mockFunc: func(mockNetworking istiocliv1beta1.NetworkingV1beta1Interface) {
105+
mockVirtualService := mockNetworking.VirtualServices("default").(*istiocliv1beta1fake.FakeVirtualServices)
106+
mockVirtualService.Fake.PrependReactor("get", "virtualservices", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
107+
return true, validVirtualService, nil
108+
})
109+
},
110+
args: args{
111+
ctx: context.Background(),
112+
namespace: "default",
113+
name: "valid",
114+
},
115+
want: validVirtualService,
116+
wantErr: false,
117+
},
118+
{
119+
name: "not found",
120+
fields: fields{
121+
networking: clientSet.NetworkingV1beta1(),
122+
},
123+
mockFunc: func(mockNetworking istiocliv1beta1.NetworkingV1beta1Interface) {
124+
mockVirtualService := mockNetworking.VirtualServices("default").(*istiocliv1beta1fake.FakeVirtualServices)
125+
mockVirtualService.Fake.PrependReactor("get", "virtualservices", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) {
126+
return true, nil, errors.New("virtualservice not found")
127+
})
128+
},
129+
args: args{
130+
ctx: context.Background(),
131+
namespace: "default",
132+
name: "not-exist",
133+
},
134+
want: nil,
135+
wantErr: true,
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
c, _ := newClient(tt.fields.networking)
142+
143+
tt.mockFunc(c.networking)
144+
145+
got, err := c.GetVirtualService(tt.args.ctx, tt.args.namespace, tt.args.name)
146+
if (err != nil) != tt.wantErr {
147+
t.Errorf("client.GetVirtualService() error = %v, wantErr %v", err, tt.wantErr)
148+
return
149+
}
150+
if !reflect.DeepEqual(got, tt.want) {
151+
t.Errorf("client.GetVirtualService() = %v, want %v", got, tt.want)
152+
}
153+
})
154+
}
155+
}
156+
79157
func Test_client_CreateVirtualService(t *testing.T) {
80158
clientSet := istiofake.Clientset{}
81159
type fields struct {

api/istio/mocks/client.go

Lines changed: 66 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/service/model_endpoint_service.go

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ func (s *modelEndpointsService) DeployEndpoint(ctx context.Context, model *model
124124
return nil, fmt.Errorf("unable to find istio client for environment: %s", endpoint.EnvironmentName)
125125
}
126126

127+
// if there's error during VirtualService creation, we will rollback the changes, to make sure
128+
// there's no hanging state of vs and to align with the database state
129+
defer func() {
130+
if err != nil {
131+
if err := istioClient.DeleteVirtualService(context.WithoutCancel(ctx), model.Project.Name, model.Name); err != nil {
132+
log.Errorf("failed to rollback create VirtualService: %v", err)
133+
}
134+
}
135+
}()
136+
127137
// Deploy Istio's VirtualService
128138
vs, err = istioClient.CreateVirtualService(ctx, model.Project.Name, vs)
129139
if err != nil {
@@ -159,19 +169,36 @@ func (s *modelEndpointsService) UpdateEndpoint(ctx context.Context, model *model
159169
return nil, errors.Wrapf(err, "failed to assign version endpoint to model endpoint")
160170
}
161171

172+
istioClient, ok := s.istioClients[newEndpoint.EnvironmentName]
173+
if !ok {
174+
log.Errorf("unable to find istio client for environment: %s", newEndpoint.EnvironmentName)
175+
return nil, fmt.Errorf("unable to find istio client for environment: %s", newEndpoint.EnvironmentName)
176+
}
177+
178+
// Get current VirtualService
179+
currentVs, err := istioClient.GetVirtualService(ctx, model.Project.Name, model.Name)
180+
if client.IgnoreNotFound(err) != nil {
181+
log.Errorf("failed to get VirtualService: %v", err)
182+
return nil, errors.Wrapf(err, "failed to get VirtualService resource on cluster")
183+
}
184+
185+
// rollback to previous virtualService state if there's error during update
186+
defer func() {
187+
if err != nil && currentVs != nil {
188+
s.cleanVirtualServiceFields(currentVs)
189+
if _, err := istioClient.PatchVirtualService(context.WithoutCancel(ctx), model.Project.Name, currentVs); err != nil {
190+
log.Errorf("failed to rollback update VirtualService: %v", err)
191+
}
192+
}
193+
}()
194+
162195
// Patch Istio's VirtualService
163196
vs, err := s.createVirtualService(model, newEndpoint)
164197
if err != nil {
165198
log.Errorf("failed to create VirtualService specification: %v", err)
166199
return nil, errors.Wrapf(err, "failed to create VirtualService specification")
167200
}
168201

169-
istioClient, ok := s.istioClients[newEndpoint.EnvironmentName]
170-
if !ok {
171-
log.Errorf("unable to find istio client for environment: %s", newEndpoint.EnvironmentName)
172-
return nil, fmt.Errorf("unable to find istio client for environment: %s", newEndpoint.EnvironmentName)
173-
}
174-
175202
// Update Istio's VirtualService
176203
vs, err = istioClient.PatchVirtualService(ctx, model.Project.Name, vs)
177204
if err != nil {
@@ -208,8 +235,24 @@ func (s *modelEndpointsService) UndeployEndpoint(ctx context.Context, model *mod
208235
return nil, fmt.Errorf("unable to find istio client for environment: %s", endpoint.EnvironmentName)
209236
}
210237

238+
currentVs, err := istioClient.GetVirtualService(ctx, model.Project.Name, model.Name)
239+
if client.IgnoreNotFound(err) != nil {
240+
log.Errorf("failed to get VirtualService: %v", err)
241+
return endpoint, nil
242+
}
243+
244+
// rollback to previous virtualService state if there's error during delete
245+
defer func() {
246+
if err != nil && currentVs != nil {
247+
s.cleanVirtualServiceFields(currentVs)
248+
if _, err := istioClient.CreateVirtualService(context.WithoutCancel(ctx), model.Project.Name, currentVs); err != nil {
249+
log.Errorf("failed to rollback delete VirtualService: %v", err)
250+
}
251+
}
252+
}()
253+
211254
// Delete Istio's VirtualService
212-
err := istioClient.DeleteVirtualService(ctx, model.Project.Name, model.Name)
255+
err = istioClient.DeleteVirtualService(ctx, model.Project.Name, model.Name)
213256
if client.IgnoreNotFound(err) != nil {
214257
log.Errorf("failed to delete VirtualService: %v", err)
215258
return nil, errors.Wrapf(err, "failed to delete VirtualService resource on cluster")
@@ -342,6 +385,23 @@ func (c *modelEndpointsService) assignVersionEndpoint(ctx context.Context, endpo
342385
return endpoint, nil
343386
}
344387

388+
// cleanVirtualServiceFields reset fields that should not be sent in update request
389+
// otherwise, the request will be rejected by the API server
390+
func (c *modelEndpointsService) cleanVirtualServiceFields(vs *v1beta1.VirtualService) {
391+
if vs == nil {
392+
return
393+
}
394+
395+
vs.SetResourceVersion("")
396+
vs.SetUID("")
397+
vs.SetSelfLink("")
398+
vs.SetCreationTimestamp(metav1.Time{})
399+
vs.SetGeneration(0)
400+
vs.SetManagedFields(nil)
401+
vs.SetOwnerReferences(nil)
402+
vs.SetFinalizers(nil)
403+
}
404+
345405
func createHttpRoutes(versionEndpointPath string, httpRouteDestinations []*istiov1beta1.HTTPRouteDestination, value protocol.Protocol) []*istiov1beta1.HTTPRoute {
346406
switch value {
347407
case protocol.UpiV1:

0 commit comments

Comments
 (0)