Skip to content

Commit 07ccd6e

Browse files
Merge pull request #62 from nicholasjackson/dev
Ensure deployments match namespace and enable wildcard matching for kubernetes deployments
2 parents 5284c6c + 17b4f8c commit 07ccd6e

File tree

11 files changed

+184
-79
lines changed

11 files changed

+184
-79
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

changelog.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.3 - 2022-05-03
11+
12+
### Changed
13+
- Ensure deployments are in the same namespace as a release
14+
- Enable wildcard matching for deployment name
15+
16+
```yaml
17+
---
18+
apiVersion: consul-release-controller.nicholasjackson.io/v1
19+
kind: Release
20+
metadata:
21+
name: payments
22+
namespace: default
23+
spec:
24+
releaser:
25+
pluginName: "consul"
26+
config:
27+
consulService: "payments"
28+
# namespace: "mynamespace"
29+
# partition: "mypartition"
30+
runtime:
31+
pluginName: "kubernetes"
32+
config:
33+
deployment: "payments-(.*)"
34+
strategy:
35+
pluginName: "canary"
36+
config:
37+
initialDelay: "30s"
38+
initialTraffic: 10
39+
interval: "30s"
40+
trafficStep: 20
41+
maxTraffic: 100
42+
errorThreshold: 5
43+
monitor:
44+
pluginName: "prometheus"
45+
config:
46+
address: "http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090"
47+
queries:
48+
- name: "request-success"
49+
preset: "envoy-request-success"
50+
min: 99
51+
- name: "request-duration"
52+
preset: "envoy-request-duration"
53+
min: 20
54+
max: 200
55+
```
56+
57+
## [0.1.2 - 2022-05-01
58+
1059
### Changed
1160
- Helm chart Webhook config failure policy now defaults to `Ignore`
1261
- Configuration for the server moved to global `config` package

clients/consul.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,14 @@ func (c *ConsulImpl) CreateServiceIntention(name string) error {
448448
if ce != nil {
449449
// we have an existing entry, mutate rather than overwrite
450450
defaults = ce.(*api.ServiceIntentionsConfigEntry)
451+
452+
// first check to see if the source already exists, if so exit
453+
for _, s := range defaults.Sources {
454+
if s.Name == ControllerServiceName {
455+
// intention already exists, exit
456+
return nil
457+
}
458+
}
451459
}
452460

453461
// update the list of intentions adding the controller intention

docs/docs/example_app.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,8 @@ running, at present the only supported runtime is `kubernetes`, however, other r
16071607
##### config
16081608
| parameter | required | type | values | description |
16091609
| ---------- | -------- | ------ | ------ | --------------------------------------------------------------- |
1610-
| deployment | yes | string | | name of the deployment that will be managed by the controller |
1610+
| deployment | yes | string | | name of the deployment that will be managed by the controller, can also contain regular expressions, for example
1611+
a deployment value of test-(.*) would match test-v1 and test-v2 |
16111612

16121613
#### strategy
16131614

functional_tests/tests.log

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

kubernetes/controller/validatingwebhook.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"net/http"
7+
"regexp"
8+
"strings"
79

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

4345
// was the deployment modified by the release controller, if so, ignore
44-
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+
4550
a.log.Debug("Ignore deployment, resource was modified by the controller", "name", deployment.Name, "namespace", deployment.Namespace, "labels", deployment.Labels)
4651

4752
return admission.Allowed("resource modified by controller")
@@ -58,15 +63,31 @@ func (a *deploymentAdmission) Handle(ctx context.Context, req admission.Request)
5863
conf := &kubernetes.PluginConfig{}
5964
json.Unmarshal(rel.Runtime.Config, conf)
6065

61-
if conf.Deployment == deployment.Name {
66+
// PluginConfig.Deployment can reference deployments using regular expressions
67+
// 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+
74+
re, err := regexp.Compile(conf.Deployment)
75+
if err != nil {
76+
a.log.Error("Invalid regular expression for deployment in release config", "release", rel.Name, "error", err)
77+
continue
78+
}
79+
80+
a.log.Debug("Checking release", "name", deployment.Name, "namespace", deployment.Namespace, "regex", conf.Deployment)
81+
82+
if re.MatchString(deployment.Name) && conf.Namespace == deployment.Namespace {
6283
// found a release for this deployment, check the state
6384
sm, err := a.provider.GetStateMachine(rel)
6485
if err != nil {
6586
a.log.Error("Error fetching statemachine", "name", deployment.Name, "namespace", deployment.Namespace, "error", err)
6687
return admission.Errored(500, err)
6788
}
6889

69-
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())
7091

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

kubernetes/controller/validatingwebhook_test.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import (
1616
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
1717
)
1818

19-
func setupAdmission(t *testing.T) (*deploymentAdmission, *mocks.Mocks) {
19+
func setupAdmission(t *testing.T, deploymentName, namespace string) (*deploymentAdmission, *mocks.Mocks) {
2020
pm, mm := mocks.BuildMocks(t)
2121

2222
pc := &kubernetes.PluginConfig{}
23-
pc.Deployment = "test-deployment"
23+
pc.Deployment = deploymentName
24+
pc.Namespace = namespace
2425

2526
pcd, _ := json.Marshal(pc)
2627

@@ -57,6 +58,7 @@ func createAdmissionRequest(withVersionLabels bool) admission.Request {
5758
ar.AdmissionRequest.Name = "test-deployment"
5859

5960
dep := &appsv1.Deployment{}
61+
dep.Namespace = "default"
6062
dep.Name = "test-deployment"
6163
dep.Labels = map[string]string{"app": "test"}
6264

@@ -74,7 +76,16 @@ func createAdmissionRequest(withVersionLabels bool) admission.Request {
7476

7577
func TestIgnoresDeploymentModifiedByControllerWhenActive(t *testing.T) {
7678
ar := createAdmissionRequest(true)
77-
d, mm := setupAdmission(t)
79+
d, mm := setupAdmission(t, "test-deployment", "default")
80+
81+
resp := d.Handle(context.TODO(), ar)
82+
require.True(t, resp.Allowed)
83+
mm.StateMachineMock.AssertNotCalled(t, "Deploy")
84+
}
85+
86+
func TestDoesNothingForNewDeploymentWithNamespaceMismatch(t *testing.T) {
87+
ar := createAdmissionRequest(false)
88+
d, mm := setupAdmission(t, "test-deployment", "mine")
7889

7990
resp := d.Handle(context.TODO(), ar)
8091
require.True(t, resp.Allowed)
@@ -83,7 +94,28 @@ func TestIgnoresDeploymentModifiedByControllerWhenActive(t *testing.T) {
8394

8495
func TestCallsDeployForNewDeploymentWhenIdle(t *testing.T) {
8596
ar := createAdmissionRequest(false)
86-
d, mm := setupAdmission(t)
97+
d, mm := setupAdmission(t, "test-deployment", "default")
98+
99+
resp := d.Handle(context.TODO(), ar)
100+
require.True(t, resp.Allowed)
101+
mm.StateMachineMock.AssertCalled(t, "Deploy")
102+
}
103+
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+
116+
func TestCallsDeployForNewDeploymentWhenIdleAndUsingRegularExpressions(t *testing.T) {
117+
ar := createAdmissionRequest(false)
118+
d, mm := setupAdmission(t, "test-(.*)", "default")
87119

88120
resp := d.Handle(context.TODO(), ar)
89121
require.True(t, resp.Allowed)
@@ -92,7 +124,7 @@ func TestCallsDeployForNewDeploymentWhenIdle(t *testing.T) {
92124

93125
func TestCallsDeployForNewDeploymentWhenFailed(t *testing.T) {
94126
ar := createAdmissionRequest(false)
95-
d, mm := setupAdmission(t)
127+
d, mm := setupAdmission(t, "test-deployment", "default")
96128

97129
testutils.ClearMockCall(&mm.StateMachineMock.Mock, "CurrentState")
98130
mm.StateMachineMock.On("CurrentState").Return(interfaces.StateFail)
@@ -104,7 +136,7 @@ func TestCallsDeployForNewDeploymentWhenFailed(t *testing.T) {
104136

105137
func TestReturnsAllowedWhenReleaseNotFound(t *testing.T) {
106138
ar := createAdmissionRequest(false)
107-
d, mm := setupAdmission(t)
139+
d, mm := setupAdmission(t, "test-deployment", "default")
108140

109141
testutils.ClearMockCall(&mm.StoreMock.Mock, "ListReleases")
110142
mm.StoreMock.On("ListReleases", &interfaces.ListOptions{"kubernetes"}).Return(
@@ -119,7 +151,7 @@ func TestReturnsAllowedWhenReleaseNotFound(t *testing.T) {
119151

120152
func TestReturnsDeniedWhenReleaseActive(t *testing.T) {
121153
ar := createAdmissionRequest(false)
122-
d, mm := setupAdmission(t)
154+
d, mm := setupAdmission(t, "test-deployment", "default")
123155

124156
testutils.ClearMockCall(&mm.StateMachineMock.Mock, "CurrentState")
125157
mm.StateMachineMock.On("CurrentState").Return(interfaces.StateMonitor)

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)

0 commit comments

Comments
 (0)