Skip to content

Commit

Permalink
Merge pull request #62 from nicholasjackson/dev
Browse files Browse the repository at this point in the history
Ensure deployments match namespace and enable wildcard matching for kubernetes deployments
  • Loading branch information
nicholasjackson authored May 4, 2022
2 parents 5284c6c + 17b4f8c commit 07ccd6e
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 79 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
docs/.docusaurus
docs/build
docs/build
functional_tests/tests.log
49 changes: 49 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.3 - 2022-05-03

### Changed
- Ensure deployments are in the same namespace as a release
- Enable wildcard matching for deployment name

```yaml
---
apiVersion: consul-release-controller.nicholasjackson.io/v1
kind: Release
metadata:
name: payments
namespace: default
spec:
releaser:
pluginName: "consul"
config:
consulService: "payments"
# namespace: "mynamespace"
# partition: "mypartition"
runtime:
pluginName: "kubernetes"
config:
deployment: "payments-(.*)"
strategy:
pluginName: "canary"
config:
initialDelay: "30s"
initialTraffic: 10
interval: "30s"
trafficStep: 20
maxTraffic: 100
errorThreshold: 5
monitor:
pluginName: "prometheus"
config:
address: "http://prometheus-kube-prometheus-prometheus.monitoring.svc:9090"
queries:
- name: "request-success"
preset: "envoy-request-success"
min: 99
- name: "request-duration"
preset: "envoy-request-duration"
min: 20
max: 200
```
## [0.1.2 - 2022-05-01
### Changed
- Helm chart Webhook config failure policy now defaults to `Ignore`
- Configuration for the server moved to global `config` package
Expand Down
8 changes: 8 additions & 0 deletions clients/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,14 @@ func (c *ConsulImpl) CreateServiceIntention(name string) error {
if ce != nil {
// we have an existing entry, mutate rather than overwrite
defaults = ce.(*api.ServiceIntentionsConfigEntry)

// first check to see if the source already exists, if so exit
for _, s := range defaults.Sources {
if s.Name == ControllerServiceName {
// intention already exists, exit
return nil
}
}
}

// update the list of intentions adding the controller intention
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/example_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -1607,7 +1607,8 @@ running, at present the only supported runtime is `kubernetes`, however, other r
##### config
| parameter | required | type | values | description |
| ---------- | -------- | ------ | ------ | --------------------------------------------------------------- |
| deployment | yes | string | | name of the deployment that will be managed by the controller |
| deployment | yes | string | | name of the deployment that will be managed by the controller, can also contain regular expressions, for example
a deployment value of test-(.*) would match test-v1 and test-v2 |

#### strategy

Expand Down
50 changes: 0 additions & 50 deletions functional_tests/tests.log

This file was deleted.

27 changes: 24 additions & 3 deletions kubernetes/controller/validatingwebhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"net/http"
"regexp"
"strings"

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

// was the deployment modified by the release controller, if so, ignore
if deployment.Labels != nil && deployment.Labels["consul-release-controller-version"] != "" && deployment.Labels["consul-release-controller-version"] == deployment.ResourceVersion {
if deployment.Labels != nil &&
deployment.Labels[interfaces.RuntimeDeploymentVersionLabel] != "" &&
deployment.Labels[interfaces.RuntimeDeploymentVersionLabel] == deployment.ResourceVersion {

a.log.Debug("Ignore deployment, resource was modified by the controller", "name", deployment.Name, "namespace", deployment.Namespace, "labels", deployment.Labels)

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

if conf.Deployment == deployment.Name {
// PluginConfig.Deployment can reference deployments using regular expressions
// check if this matches

//first check to see if the regex terminates in $ (word boundary), if not add it
if !strings.HasSuffix(conf.Deployment, "$") {
conf.Deployment = conf.Deployment + "$"
}

re, err := regexp.Compile(conf.Deployment)
if err != nil {
a.log.Error("Invalid regular expression for deployment in release config", "release", rel.Name, "error", err)
continue
}

a.log.Debug("Checking release", "name", deployment.Name, "namespace", deployment.Namespace, "regex", conf.Deployment)

if re.MatchString(deployment.Name) && conf.Namespace == deployment.Namespace {
// found a release for this deployment, check the state
sm, err := a.provider.GetStateMachine(rel)
if err != nil {
a.log.Error("Error fetching statemachine", "name", deployment.Name, "namespace", deployment.Namespace, "error", err)
return admission.Errored(500, err)
}

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

if sm.CurrentState() == interfaces.StateIdle || sm.CurrentState() == interfaces.StateFail {
// kick off a new deployment
Expand Down
46 changes: 39 additions & 7 deletions kubernetes/controller/validatingwebhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

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

pc := &kubernetes.PluginConfig{}
pc.Deployment = "test-deployment"
pc.Deployment = deploymentName
pc.Namespace = namespace

pcd, _ := json.Marshal(pc)

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

dep := &appsv1.Deployment{}
dep.Namespace = "default"
dep.Name = "test-deployment"
dep.Labels = map[string]string{"app": "test"}

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

func TestIgnoresDeploymentModifiedByControllerWhenActive(t *testing.T) {
ar := createAdmissionRequest(true)
d, mm := setupAdmission(t)
d, mm := setupAdmission(t, "test-deployment", "default")

resp := d.Handle(context.TODO(), ar)
require.True(t, resp.Allowed)
mm.StateMachineMock.AssertNotCalled(t, "Deploy")
}

func TestDoesNothingForNewDeploymentWithNamespaceMismatch(t *testing.T) {
ar := createAdmissionRequest(false)
d, mm := setupAdmission(t, "test-deployment", "mine")

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

func TestCallsDeployForNewDeploymentWhenIdle(t *testing.T) {
ar := createAdmissionRequest(false)
d, mm := setupAdmission(t)
d, mm := setupAdmission(t, "test-deployment", "default")

resp := d.Handle(context.TODO(), ar)
require.True(t, resp.Allowed)
mm.StateMachineMock.AssertCalled(t, "Deploy")
}

func TestAddsRegExpWordBoundaryAndFailsMatchWhenNotPresent(t *testing.T) {
ar := createAdmissionRequest(false)

// a regexp without a word boundary would match, check we add
// the word boundary when not present
d, mm := setupAdmission(t, "test-", "default")

resp := d.Handle(context.TODO(), ar)
require.True(t, resp.Allowed)
mm.StateMachineMock.AssertNotCalled(t, "Deploy")
}

func TestCallsDeployForNewDeploymentWhenIdleAndUsingRegularExpressions(t *testing.T) {
ar := createAdmissionRequest(false)
d, mm := setupAdmission(t, "test-(.*)", "default")

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

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

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

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

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

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

testutils.ClearMockCall(&mm.StateMachineMock.Mock, "CurrentState")
mm.StateMachineMock.On("CurrentState").Return(interfaces.StateMonitor)
Expand Down
1 change: 1 addition & 0 deletions plugins/interfaces/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
RuntimeDeploymentNoAction RuntimeDeploymentStatus = "runtime_deployment_no_action"
RuntimeDeploymentNotFound RuntimeDeploymentStatus = "runtime_deployment_not_found"
RuntimeDeploymentInternalError RuntimeDeploymentStatus = "runtime_deployment_internal_error"
RuntimeDeploymentVersionLabel = "consul-release-controller-version"
)

// RuntimeBaseConfig is the base configuration that all runtime plugins must implement
Expand Down
17 changes: 17 additions & 0 deletions plugins/kubernetes/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ func (p *Plugin) InitPrimary(ctx context.Context) (interfaces.RuntimeDeploymentS
primaryDeployment.Name = primaryName
primaryDeployment.ResourceVersion = ""

// add labels to ensure the deployment is not picked up by the validating webhook
if primaryDeployment.Labels == nil {
primaryDeployment.Labels = map[string]string{}
}

primaryDeployment.Labels[interfaces.RuntimeDeploymentVersionLabel] = "1"

// save the new primary
err = p.kubeClient.UpsertDeployment(ctx, primaryDeployment)
if err != nil {
Expand Down Expand Up @@ -163,6 +170,13 @@ func (p *Plugin) PromoteCandidate(ctx context.Context) (interfaces.RuntimeDeploy
primary.Name = primaryName
primary.ResourceVersion = ""

// add labels to ensure the deployment is not picked up by the validating webhook
if primary.Labels == nil {
primary.Labels = map[string]string{}
}

primary.Labels[interfaces.RuntimeDeploymentVersionLabel] = "1"

// save the new deployment
err = p.kubeClient.UpsertDeployment(ctx, primary)
if err != nil {
Expand Down Expand Up @@ -255,6 +269,9 @@ func (p *Plugin) RestoreOriginal(ctx context.Context) error {
cd.Name = p.config.Deployment
cd.ResourceVersion = ""

// remove the ownership label so that it can be updated as normal
delete(cd.Labels, interfaces.RuntimeDeploymentVersionLabel)

p.log.Debug("Clone primary to create original deployment", "name", p.config.Deployment, "namespace", p.config.Namespace)

err = p.kubeClient.UpsertDeployment(ctx, cd)
Expand Down
Loading

0 comments on commit 07ccd6e

Please sign in to comment.