Skip to content

Commit

Permalink
feature: EnforcePodAnnotations (#13)
Browse files Browse the repository at this point in the history
* logging: log listener type (HTTP vs HTTPS)

* wip: EnforcePodAnnotations

* wip: TestEnforcePodAnnotations

* deps: update dependencies

* wip: EnforcePodAnnotations now handles all core Pod-related Kinds

* Add a sample manifest for EnforcePodAnnotaions & fix docker image for AC

* Add EnforcePodAnnotations to example server; update README

* ci: set GOPROXY

* build: use GOPROXY in Dockerfile + ARGs

* Handle panics in the logging middleware

* wip: update sample YAML manifests for EnforcePodAnnotations

* wip: fix panic on AdmitFunc errors when nil resp returned

* deps: update k8s API dependencies

* build: update deps

* tests: add handler tests; update AdmitFunc tests for EnforcePodAnnotations

* build: remove GOPROXY due to 410 errors

* tests: add test comments

* build: update .gitignore

* tests: Improve server tests for cancellation

* tests: handle nil AdmissionResponses

* docs: DenyIngress - godoc clarity

* build: update deps

* build: fix gofmt issue

* test: fix namespace/annotation access; add DaemonSet tests

* build: remove refs from CI config (unused)

* deps: update deps

* deps: update k8s deps

* tests: use podDeniedError; first draft of EnforcePodAnnotations tests

* build: build the container concurrently

* tests: standardize test error messages

* docs: improve AdmitFunc example in README

* docs: update unannotated deployment sample
  • Loading branch information
elithrar committed Aug 3, 2019
1 parent 2e7427b commit 2da0e28
Show file tree
Hide file tree
Showing 17 changed files with 853 additions and 200 deletions.
24 changes: 12 additions & 12 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
version: 2.1

jobs:
# Base test configuration for Go library tests Each distinct version should
# inherit this base, and override (at least) the container image used.
"test": &test
docker:
- image: "circleci/golang:<< parameters.v >>"
working_directory: /go/src/github.com/elithrar/admission-control
environment:
GO111MODULE: "on"
"test":
parameters:
v:
type: string
Expand All @@ -22,6 +15,15 @@ jobs:
modules:
type: boolean
default: true
goproxy:
type: string
default: ""
docker:
- image: "circleci/golang:<< parameters.v >>"
working_directory: /go/src/github.com/elithrar/admission-control
environment:
GO111MODULE: "on"
GOPROXY: "<< parameters.goproxy >>"
steps:
- checkout
- run:
Expand Down Expand Up @@ -62,7 +64,7 @@ jobs:
command: >
go test -v -race ./...
"build-container": &build-container
"build-container":
docker:
- image: docker:18
working_directory: /go/src/github.com/elithrar/admission-control
Expand All @@ -88,6 +90,4 @@ workflows:
- test:
name: "v1.12"
v: "1.12"
- "build-container":
requires:
- "latest"
- "build-container"
97 changes: 12 additions & 85 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,88 +1,15 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Test binary, built with `go test -c`
*.test

# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/
# Dependency directories (remove the comment below to include it)
# vendor/
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
FROM golang:1.12 as build

ARG GIT_COMMIT=""
LABEL commit=$GIT_COMMIT
ENV GIT_COMMIT=$GIT_COMMIT

WORKDIR /go/src/app
COPY go.mod .
COPY go.sum .

ENV GO111MODULE=on
#ENV GOPROXY="https://proxy.golang.org"
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -v ./...
Expand Down
67 changes: 54 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,27 @@ A micro-framework for building Kubernetes [Admission Controllers](https://kubern

### Built-In AdmitFuncs

Admission Control provides a number of useful built-in _AdmitFuncs_, including:

- `DenyPublicLoadBalancers` - prevents exposing `Services` of `type: LoadBalancer` outside of the cluster; instead requiring the LB to be annotated as internal-only.
- `DenyIngresses` - similar to the above, it prevents creating Ingresses (except in the namespaces you allow)

More built-ins are coming soon! ⏳
Admission Control provides a number of useful built-in [**AdmitFuncs**](https://godoc.org/github.com/elithrar/admission-control#AdmitFunc), including:

- `EnforcePodAnnotations` - ensures that admitted Pods have (at least) the
required set of annotations. Annotation _values_ are matched using a
`matchFunc` (a `func(string) bool`) that allows flexible matching. For
example, a matchFunc could wrap the
[`IsDomainName`](https://godoc.org/github.com/miekg/dns#IsDomainName)
function from `miekg/dns`, or reference a `[]string` of accepted values. It
is strongly suggested you use a
[`namespaceSelector`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.13/#webhook-v1beta1-admissionregistration)
as part of your webhook configuration to only apply this to specific
namespaces, and/or set the `ignoreNamespaces` argument to include
`kube-system`, as annotation validation will otherwise include system Pods.
- `DenyPublicLoadBalancers` - prevents exposing `Services` of `type: LoadBalancer` outside of the cluster, instead requiring the LB to be
annotated as internal-only, by looking for the well-known annotations for
major cloud providers.
- `DenyIngresses` - similar to the above, it prevents creating Ingresses
(except in the namespaces you allow). This can be useful for limiting which
namespaces can expose services via common Ingress types.

More built-ins are coming soon, and suggestions are welcome! ⏳

### Creating Your Own AdmitFunc

Expand All @@ -48,17 +63,43 @@ An example `AdmitFunc` looks like this:
func DenyDefaultLoadBalancerSourceRanges() AdmitFunc {
// Return a function of type AdmitFunc
return func(admissionReview *admission.AdmissionReview) (*admission.AdmissionResponse, error) {
// do work

// returning a non-nil AdmissionResponse and a nil error will allow admission.

// returning an error will deny Admission; the error string will be
// provided to the client and should be clear about why we rejected
// them.
kind := admissionReview.Request.Kind.Kind
// Create an *admission.AdmissionResponse that denies by default.
resp := newDefaultDenyResponse()

// Create an object to deserialize our requests' object into
service := core.Service{}
deserializer := serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
if _, _, err := deserializer.Decode(admissionReview.Request.Object.Raw, nil, &service); err != nil {
return nil, err
}

// Allow non-LoadBalancer Services to pass through.
if service.Spec.Type != "LoadBalancer" {
resp.Allowed = true
resp.Result.Message = fmt.Sprintf(
"received a non-LoadBalancer type (%s)",
service.Spec.Type,
)
return resp, nil
}

// Inspect the service.Spec.LoadBalancerSourceRanges field
// If unset, reject it.
// Returning an error from an AdmitFunc will automatically deny admission of that requests' object.
if service.Spec.LoadBalancerSourceRanges == nil {
return resp, fmt.Errorf("LoadBalancers without explicitly configured LoadBalancerSourceRanges are not allowed.")
}

// Set resp.Allowed to true before returning your AdmissionResponse
resp.Allowed = true
return resp, nil
}
}
```

You can see that we deserialize the raw object in our `AdmissionReview` into an object (based on its Kind), inspect and validate the fields we're interested in, and either return an error (rejecting admission) or set `resp.Allowed = true` and allow admission.

Tips:

- Having your `AdmitFunc`s focus on "one" thing is best practice: it allows you to be more granular in how you apply constraints to your cluster
Expand Down
Loading

0 comments on commit 2da0e28

Please sign in to comment.