Skip to content

Commit 17b4f8c

Browse files
Ensure wordboundary present on regexp
1 parent d375b5f commit 17b4f8c

File tree

7 files changed

+87
-70
lines changed

7 files changed

+87
-70
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
docs/.docusaurus
3-
docs/build
3+
docs/build
4+
functional_tests/tests.log

functional_tests/tests.log

Lines changed: 0 additions & 50 deletions
This file was deleted.

kubernetes/controller/validatingwebhook.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"net/http"
77
"regexp"
8+
"strings"
89

910
"github.com/hashicorp/go-hclog"
1011
"github.com/nicholasjackson/consul-release-controller/plugins/interfaces"
@@ -42,7 +43,10 @@ func (a *deploymentAdmission) Handle(ctx context.Context, req admission.Request)
4243
a.log.Debug("Handle deployment admission", "deployment", deployment.Name, "namespaces", deployment.Namespace)
4344

4445
// was the deployment modified by the release controller, if so, ignore
45-
if deployment.Labels != nil && deployment.Labels["consul-release-controller-version"] != "" && deployment.Labels["consul-release-controller-version"] == deployment.ResourceVersion {
46+
if deployment.Labels != nil &&
47+
deployment.Labels[interfaces.RuntimeDeploymentVersionLabel] != "" &&
48+
deployment.Labels[interfaces.RuntimeDeploymentVersionLabel] == deployment.ResourceVersion {
49+
4650
a.log.Debug("Ignore deployment, resource was modified by the controller", "name", deployment.Name, "namespace", deployment.Namespace, "labels", deployment.Labels)
4751

4852
return admission.Allowed("resource modified by controller")
@@ -61,12 +65,20 @@ func (a *deploymentAdmission) Handle(ctx context.Context, req admission.Request)
6165

6266
// PluginConfig.Deployment can reference deployments using regular expressions
6367
// check if this matches
68+
69+
//first check to see if the regex terminates in $ (word boundary), if not add it
70+
if !strings.HasSuffix(conf.Deployment, "$") {
71+
conf.Deployment = conf.Deployment + "$"
72+
}
73+
6474
re, err := regexp.Compile(conf.Deployment)
6575
if err != nil {
6676
a.log.Error("Invalid regular expression for deployment in release config", "release", rel.Name, "error", err)
6777
continue
6878
}
6979

80+
a.log.Debug("Checking release", "name", deployment.Name, "namespace", deployment.Namespace, "regex", conf.Deployment)
81+
7082
if re.MatchString(deployment.Name) && conf.Namespace == deployment.Namespace {
7183
// found a release for this deployment, check the state
7284
sm, err := a.provider.GetStateMachine(rel)
@@ -75,7 +87,7 @@ func (a *deploymentAdmission) Handle(ctx context.Context, req admission.Request)
7587
return admission.Errored(500, err)
7688
}
7789

78-
a.log.Debug("Found existing release", "name", deployment.Name, "namespace", deployment.Namespace, "state", sm.CurrentState())
90+
a.log.Debug("Found existing release for", "name", deployment.Name, "namespace", deployment.Namespace, "state", sm.CurrentState())
7991

8092
if sm.CurrentState() == interfaces.StateIdle || sm.CurrentState() == interfaces.StateFail {
8193
// kick off a new deployment

kubernetes/controller/validatingwebhook_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ func TestCallsDeployForNewDeploymentWhenIdle(t *testing.T) {
101101
mm.StateMachineMock.AssertCalled(t, "Deploy")
102102
}
103103

104+
func TestAddsRegExpWordBoundaryAndFailsMatchWhenNotPresent(t *testing.T) {
105+
ar := createAdmissionRequest(false)
106+
107+
// a regexp without a word boundary would match, check we add
108+
// the word boundary when not present
109+
d, mm := setupAdmission(t, "test-", "default")
110+
111+
resp := d.Handle(context.TODO(), ar)
112+
require.True(t, resp.Allowed)
113+
mm.StateMachineMock.AssertNotCalled(t, "Deploy")
114+
}
115+
104116
func TestCallsDeployForNewDeploymentWhenIdleAndUsingRegularExpressions(t *testing.T) {
105117
ar := createAdmissionRequest(false)
106118
d, mm := setupAdmission(t, "test-(.*)", "default")

plugins/interfaces/runtime.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
RuntimeDeploymentNoAction RuntimeDeploymentStatus = "runtime_deployment_no_action"
1212
RuntimeDeploymentNotFound RuntimeDeploymentStatus = "runtime_deployment_not_found"
1313
RuntimeDeploymentInternalError RuntimeDeploymentStatus = "runtime_deployment_internal_error"
14+
RuntimeDeploymentVersionLabel = "consul-release-controller-version"
1415
)
1516

1617
// RuntimeBaseConfig is the base configuration that all runtime plugins must implement

plugins/kubernetes/plugin.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ func (p *Plugin) InitPrimary(ctx context.Context) (interfaces.RuntimeDeploymentS
108108
primaryDeployment.Name = primaryName
109109
primaryDeployment.ResourceVersion = ""
110110

111+
// add labels to ensure the deployment is not picked up by the validating webhook
112+
if primaryDeployment.Labels == nil {
113+
primaryDeployment.Labels = map[string]string{}
114+
}
115+
116+
primaryDeployment.Labels[interfaces.RuntimeDeploymentVersionLabel] = "1"
117+
111118
// save the new primary
112119
err = p.kubeClient.UpsertDeployment(ctx, primaryDeployment)
113120
if err != nil {
@@ -163,6 +170,13 @@ func (p *Plugin) PromoteCandidate(ctx context.Context) (interfaces.RuntimeDeploy
163170
primary.Name = primaryName
164171
primary.ResourceVersion = ""
165172

173+
// add labels to ensure the deployment is not picked up by the validating webhook
174+
if primary.Labels == nil {
175+
primary.Labels = map[string]string{}
176+
}
177+
178+
primary.Labels[interfaces.RuntimeDeploymentVersionLabel] = "1"
179+
166180
// save the new deployment
167181
err = p.kubeClient.UpsertDeployment(ctx, primary)
168182
if err != nil {
@@ -255,6 +269,9 @@ func (p *Plugin) RestoreOriginal(ctx context.Context) error {
255269
cd.Name = p.config.Deployment
256270
cd.ResourceVersion = ""
257271

272+
// remove the ownership label so that it can be updated as normal
273+
delete(cd.Labels, interfaces.RuntimeDeploymentVersionLabel)
274+
258275
p.log.Debug("Clone primary to create original deployment", "name", p.config.Deployment, "namespace", p.config.Namespace)
259276

260277
err = p.kubeClient.UpsertDeployment(ctx, cd)

plugins/kubernetes/plugin_test.go

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
)
1818

1919
var one = int32(1)
20-
var dep = &appsv1.Deployment{
20+
var mockDep = &appsv1.Deployment{
2121
ObjectMeta: v1.ObjectMeta{
2222
Name: "test-deployment",
2323
Namespace: "testnamespace",
@@ -26,7 +26,7 @@ var dep = &appsv1.Deployment{
2626
Spec: appsv1.DeploymentSpec{Replicas: &one},
2727
}
2828

29-
var cloneDep = &appsv1.Deployment{
29+
var mockCloneDep = &appsv1.Deployment{
3030
ObjectMeta: v1.ObjectMeta{
3131
Name: "test-deployment-primary",
3232
Namespace: "testnamespace",
@@ -35,7 +35,7 @@ var cloneDep = &appsv1.Deployment{
3535
Spec: appsv1.DeploymentSpec{Replicas: &one},
3636
}
3737

38-
func setupPlugin(t *testing.T) (*Plugin, *clients.KubernetesMock) {
38+
func setupPlugin(t *testing.T) (*Plugin, *clients.KubernetesMock, *appsv1.Deployment, *appsv1.Deployment) {
3939

4040
retryTimeout = 10 * time.Millisecond
4141
retryInterval = 1 * time.Millisecond
@@ -52,11 +52,11 @@ func setupPlugin(t *testing.T) (*Plugin, *clients.KubernetesMock) {
5252
p.config.Deployment = "test-deployment"
5353
p.config.Namespace = "testnamespace"
5454

55-
return p, km
55+
return p, km, mockDep.DeepCopy(), mockCloneDep.DeepCopy()
5656
}
5757

5858
func TestInitPrimaryDoesNothingWhenPrimaryExists(t *testing.T) {
59-
p, km := setupPlugin(t)
59+
p, km, dep, cloneDep := setupPlugin(t)
6060

6161
testutils.ClearMockCall(&km.Mock, "GetDeployment")
6262
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Return(cloneDep, nil)
@@ -72,7 +72,7 @@ func TestInitPrimaryDoesNothingWhenPrimaryExists(t *testing.T) {
7272
}
7373

7474
func TestInitPrimaryDoesNothingWhenCandidateDoesNotExist(t *testing.T) {
75-
p, km := setupPlugin(t)
75+
p, km, _, _ := setupPlugin(t)
7676

7777
testutils.ClearMockCall(&km.Mock, "GetDeployment")
7878
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Return(nil, fmt.Errorf("Primary not found"))
@@ -84,7 +84,7 @@ func TestInitPrimaryDoesNothingWhenCandidateDoesNotExist(t *testing.T) {
8484
}
8585

8686
func TestInitPrimaryCreatesPrimaryWhenCandidateExists(t *testing.T) {
87-
p, km := setupPlugin(t)
87+
p, km, dep, cloneDep := setupPlugin(t)
8888

8989
testutils.ClearMockCall(&km.Mock, "GetDeployment")
9090
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(nil, fmt.Errorf("Primary not found"))
@@ -97,10 +97,14 @@ func TestInitPrimaryCreatesPrimaryWhenCandidateExists(t *testing.T) {
9797
require.Equal(t, interfaces.RuntimeDeploymentUpdate, status)
9898

9999
km.AssertCalled(t, "UpsertDeployment", mock.Anything, mock.Anything)
100+
101+
// check that the runtimedeploymentversion label is added to ensure the validating webhook ignores this deployment
102+
depArg := getUpsertDeployment(km.Mock)
103+
require.Equal(t, depArg.Labels[interfaces.RuntimeDeploymentVersionLabel], "1")
100104
}
101105

102106
func TestPromoteCandidateDoesNothingWhenCandidateNotExists(t *testing.T) {
103-
p, km := setupPlugin(t)
107+
p, km, _, _ := setupPlugin(t)
104108

105109
testutils.ClearMockCall(&km.Mock, "GetDeployment")
106110
km.On("GetHealthyDeployment", mock.Anything, "test-deployment", "testnamespace").Return(nil, clients.ErrDeploymentNotFound)
@@ -113,7 +117,7 @@ func TestPromoteCandidateDoesNothingWhenCandidateNotExists(t *testing.T) {
113117
}
114118

115119
func TestPromoteCandidateDeletesExistingPrimaryAndUpserts(t *testing.T) {
116-
p, km := setupPlugin(t)
120+
p, km, dep, _ := setupPlugin(t)
117121

118122
testutils.ClearMockCall(&km.Mock, "GetDeployment")
119123
km.On("GetHealthyDeployment", mock.Anything, "test-deployment", "testnamespace").Once().Return(dep, nil)
@@ -127,10 +131,14 @@ func TestPromoteCandidateDeletesExistingPrimaryAndUpserts(t *testing.T) {
127131

128132
km.AssertCalled(t, "DeleteDeployment", mock.Anything, "test-deployment-primary", "testnamespace")
129133
km.AssertCalled(t, "UpsertDeployment", mock.Anything, mock.Anything)
134+
135+
// check that the runtimedeploymentversion label is added to ensure the validating webhook ignores this deployment
136+
depArg := getUpsertDeployment(km.Mock)
137+
require.Equal(t, depArg.Labels[interfaces.RuntimeDeploymentVersionLabel], "1")
130138
}
131139

132140
func TestRemoveCandidateDoesNothingWhenCandidateNotFound(t *testing.T) {
133-
p, km := setupPlugin(t)
141+
p, km, _, _ := setupPlugin(t)
134142

135143
testutils.ClearMockCall(&km.Mock, "GetDeployment")
136144
km.On("GetDeployment", mock.Anything, "test-deployment", "testnamespace").Once().Return(nil, clients.ErrDeploymentNotFound)
@@ -142,7 +150,7 @@ func TestRemoveCandidateDoesNothingWhenCandidateNotFound(t *testing.T) {
142150
}
143151

144152
func TestRemoveCandidateScalesWhenCandidateFound(t *testing.T) {
145-
p, km := setupPlugin(t)
153+
p, km, dep, _ := setupPlugin(t)
146154

147155
testutils.ClearMockCall(&km.Mock, "GetDeployment")
148156
km.On("GetDeployment", mock.Anything, "test-deployment", "testnamespace").Once().Return(dep, nil)
@@ -156,7 +164,7 @@ func TestRemoveCandidateScalesWhenCandidateFound(t *testing.T) {
156164
}
157165

158166
func TestRestoreDoesNothingWhenNoPrimaryFound(t *testing.T) {
159-
p, km := setupPlugin(t)
167+
p, km, _, _ := setupPlugin(t)
160168

161169
testutils.ClearMockCall(&km.Mock, "GetDeployment")
162170
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(nil, clients.ErrDeploymentNotFound)
@@ -166,7 +174,7 @@ func TestRestoreDoesNothingWhenNoPrimaryFound(t *testing.T) {
166174
}
167175

168176
func TestRestoreCallsDeleteWhenPrimaryFound(t *testing.T) {
169-
p, km := setupPlugin(t)
177+
p, km, _, cloneDep := setupPlugin(t)
170178

171179
testutils.ClearMockCall(&km.Mock, "GetDeployment")
172180
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(cloneDep, nil)
@@ -176,10 +184,14 @@ func TestRestoreCallsDeleteWhenPrimaryFound(t *testing.T) {
176184

177185
err := p.RestoreOriginal(context.Background())
178186
require.NoError(t, err)
187+
188+
// check that the labels are removed
189+
depArg := getUpsertDeployment(km.Mock)
190+
require.Equal(t, depArg.Labels[interfaces.RuntimeDeploymentVersionLabel], "")
179191
}
180192

181193
func TestRestoreProceedesWhenExistingCandidateNotFound(t *testing.T) {
182-
p, km := setupPlugin(t)
194+
p, km, _, cloneDep := setupPlugin(t)
183195

184196
testutils.ClearMockCall(&km.Mock, "GetDeployment")
185197
km.On("GetDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(cloneDep, nil)
@@ -192,7 +204,7 @@ func TestRestoreProceedesWhenExistingCandidateNotFound(t *testing.T) {
192204
}
193205

194206
func TestRemovePrimaryCallsDelete(t *testing.T) {
195-
p, km := setupPlugin(t)
207+
p, km, _, _ := setupPlugin(t)
196208

197209
testutils.ClearMockCall(&km.Mock, "GetDeployment")
198210
km.On("DeleteDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(nil)
@@ -202,7 +214,7 @@ func TestRemovePrimaryCallsDelete(t *testing.T) {
202214
}
203215

204216
func TestRemovePrimaryReturnsErrorWhenDeleteIsGenericError(t *testing.T) {
205-
p, km := setupPlugin(t)
217+
p, km, _, _ := setupPlugin(t)
206218

207219
testutils.ClearMockCall(&km.Mock, "GetDeployment")
208220
km.On("DeleteDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(fmt.Errorf("test"))
@@ -212,11 +224,23 @@ func TestRemovePrimaryReturnsErrorWhenDeleteIsGenericError(t *testing.T) {
212224
}
213225

214226
func TestRemovePrimaryReturnsNoErrorWhenDeleteIsNotFoundError(t *testing.T) {
215-
p, km := setupPlugin(t)
227+
p, km, _, _ := setupPlugin(t)
216228

217229
testutils.ClearMockCall(&km.Mock, "GetDeployment")
218230
km.On("DeleteDeployment", mock.Anything, "test-deployment-primary", "testnamespace").Once().Return(clients.ErrDeploymentNotFound)
219231

220232
err := p.RemovePrimary(context.Background())
221233
require.NoError(t, err)
222234
}
235+
236+
func getUpsertDeployment(mock mock.Mock) *appsv1.Deployment {
237+
for _, c := range mock.Calls {
238+
if c.Method == "UpsertDeployment" {
239+
if dep, ok := c.Arguments.Get(1).(*appsv1.Deployment); ok {
240+
return dep
241+
}
242+
}
243+
}
244+
245+
return nil
246+
}

0 commit comments

Comments
 (0)