diff --git a/.gitignore b/.gitignore index 7a1b4a85256..439b0fbb2c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,28 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +*.swp +*.swo +*~ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..0a6d7b92919 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build the manager binary +FROM golang:1.10.3 as builder + +# Copy in the go src +WORKDIR /go/src/github.com/open-policy-agent/kubernetes-policy-controller +COPY pkg/ pkg/ +COPY cmd/ cmd/ +COPY vendor/ vendor/ + +# Build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager github.com/open-policy-agent/kubernetes-policy-controller/cmd/manager + +# Copy the controller-manager into a thin image +FROM ubuntu:latest +WORKDIR /root/ +COPY --from=builder /go/src/github.com/open-policy-agent/kubernetes-policy-controller/manager . +ENTRYPOINT ["./manager"] diff --git a/Gopkg.lock b/Gopkg.lock index cb53f150545..9e7f052662d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,81 +2,334 @@ [[projects]] - branch = "master" - name = "github.com/Azure/kubernetes-policy-controller" - packages = ["pkg/policies/types"] - revision = "c066c149f81ba96765bc3313638aca7863f586d3" + digest = "1:e4549155be72f065cf860ada7148bbeb0857360e81da2d5e28b799bd8720f1bc" + name = "cloud.google.com/go" + packages = ["compute/metadata"] + pruneopts = "T" + revision = "0ebda48a7f143b1cce9eb37a8c1106ac762a3430" + version = "v0.34.0" [[projects]] - name = "github.com/dchest/siphash" + digest = "1:4f0c47f0381e507931047796253de4fb3a336514512b64a1f3e96b676dfe5f57" + name = "github.com/OneOfOne/xxhash" packages = ["."] - revision = "ca249f45189071f5d44dc6401334e3572037b9cb" - version = "v1.2.0" + pruneopts = "T" + revision = "6def279d2ce6c81a79dd1c1be580f03bb216fb8a" + version = "v1.2.2" [[projects]] + branch = "master" + digest = "1:ad4589ec239820ee99eb01c1ad47ebc5f8e02c4f5103a9b210adff9696d89f36" + name = "github.com/beorn7/perks" + packages = ["quantile"] + pruneopts = "T" + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + +[[projects]] + digest = "1:9f42202ac457c462ad8bb9642806d275af9ab4850cf0b1960b9c6f083d4a309a" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + pruneopts = "T" + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + +[[projects]] + digest = "1:0ffd93121f3971aea43f6a26b3eaaa64c8af20fb0ff0731087d8dab7164af5a8" + name = "github.com/emicklei/go-restful" + packages = [ + ".", + "log", + ] + pruneopts = "T" + revision = "3eb9738c1697594ea6e71a7156a9bb32ed216cf0" + version = "v2.8.0" + +[[projects]] + digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda" name = "github.com/ghodss/yaml" packages = ["."] + pruneopts = "T" revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" [[projects]] + branch = "master" + digest = "1:65587005c6fa4293c0b8a2e457e689df7fda48cc5e1f5449ea2c1e7784551558" + name = "github.com/go-logr/logr" + packages = ["."] + pruneopts = "T" + revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" + +[[projects]] + digest = "1:ce43ad4015e7cdad3f0e8f2c8339439dd4470859a828d2a6988b0f713699e94a" + name = "github.com/go-logr/zapr" + packages = ["."] + pruneopts = "T" + revision = "7536572e8d55209135cd5e7ccf7fce43dca217ab" + version = "v0.1.0" + +[[projects]] + digest = "1:3e34b0a53d651b06655475eb488745c912596e08557a33b83d336aab41cb6fe9" + name = "github.com/gobuffalo/envy" + packages = ["."] + pruneopts = "T" + revision = "801d7253ade1f895f74596b9a96147ed2d3b087e" + version = "v1.6.11" + +[[projects]] + digest = "1:0a5d2a670ac050354afcf572e65aceabefdebdbb90973ea729d8640fa211a9e2" + name = "github.com/gobwas/glob" + packages = [ + ".", + "compiler", + "match", + "syntax", + "syntax/ast", + "syntax/lexer", + "util/runes", + "util/strings", + ] + pruneopts = "T" + revision = "5ccd90ef52e1e632236f7326478d4faa74f99438" + version = "v0.2.3" + +[[projects]] + digest = "1:f5ccd717b5f093cbabc51ee2e7a5979b92f17d217f9031d6d64f337101c408e4" name = "github.com/gogo/protobuf" packages = [ "proto", - "sortkeys" + "sortkeys", ] - revision = "636bf0302bc95575d69441b25a2603156ffdddf1" - version = "v1.1.1" + pruneopts = "T" + revision = "4cbf7e384e768b4e01799441fdf2a706a5635ae7" + version = "v1.2.0" [[projects]] branch = "master" + digest = "1:1ba1d79f2810270045c328ae5d674321db34e3aae468eb4233883b473c5c0467" name = "github.com/golang/glog" packages = ["."] + pruneopts = "T" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" [[projects]] branch = "master" + digest = "1:8f3489cb7352125027252a6517757cbd1706523119f1e14e20741ae8d2f70428" + name = "github.com/golang/groupcache" + packages = ["lru"] + pruneopts = "T" + revision = "c65c006176ff7ff98bb916961c7abbc6b0afc0aa" + +[[projects]] + digest = "1:a2ecb56e5053d942aafc86738915fb94c9131bac848c543b8b6764365fd69080" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp", + ] + pruneopts = "T" + revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" + version = "v1.2.0" + +[[projects]] + branch = "master" + digest = "1:0bfbe13936953a98ae3cfe8ed6670d396ad81edf069a806d2f6515d7bb6950df" + name = "github.com/google/btree" + packages = ["."] + pruneopts = "T" + revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" + +[[projects]] + branch = "master" + digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb" name = "github.com/google/gofuzz" packages = ["."] + pruneopts = "T" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" [[projects]] - name = "github.com/gorilla/context" + digest = "1:236d7e1bdb50d8f68559af37dbcf9d142d56b431c9b2176d41e2a009b664cda8" + name = "github.com/google/uuid" packages = ["."] - revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" - version = "v1.1.1" + pruneopts = "T" + revision = "9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8" + version = "v1.1.0" + +[[projects]] + digest = "1:35735e2255fa34521c2a1355fb2a3a2300bc9949f487be1c1ce8ee8efcfa2d04" + name = "github.com/googleapis/gnostic" + packages = [ + "OpenAPIv2", + "compiler", + "extensions", + ] + pruneopts = "T" + revision = "7c663266750e7d82587642f65e60bc4083f1f84e" + version = "v0.2.0" [[projects]] - name = "github.com/gorilla/mux" + branch = "master" + digest = "1:8c0ceab65d43f49dce22aac0e8f670c170fc74dcf2dfba66d3a89516f7ae2c15" + name = "github.com/gregjones/httpcache" + packages = [ + ".", + "diskcache", + ] + pruneopts = "T" + revision = "c63ab54fda8f77302f8d414e19933f2b6026a089" + +[[projects]] + digest = "1:8ec8d88c248041a6df5f6574b87bc00e7e0b493881dad2e7ef47b11dc69093b5" + name = "github.com/hashicorp/golang-lru" + packages = [ + ".", + "simplelru", + ] + pruneopts = "T" + revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768" + version = "v0.5.0" + +[[projects]] + digest = "1:8f20c8dd713564fa97299fbcb77d729c6de9c33f3222812a76e6ecfaef80fd61" + name = "github.com/hpcloud/tail" + packages = [ + ".", + "ratelimiter", + "util", + "watch", + "winfile", + ] + pruneopts = "T" + revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5" + version = "v1.0.0" + +[[projects]] + digest = "1:3477d9dd8c135faab978bac762eaeafb31f28d6da97ef500d5c271966f74140a" + name = "github.com/imdario/mergo" + packages = ["."] + pruneopts = "T" + revision = "9f23e2d6bd2a77f959b2bf6acdbefd708a83a4a4" + version = "v0.3.6" + +[[projects]] + digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + pruneopts = "T" + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + version = "v1.0" + +[[projects]] + digest = "1:ee5840274624ad20cac08227aeca582fbdaf3727ef9dd37ae935706240679e09" + name = "github.com/joho/godotenv" packages = ["."] - revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" - version = "v1.6.2" + pruneopts = "T" + revision = "23d116af351c84513e1946b527c88823e476be13" + version = "v1.3.0" [[projects]] + digest = "1:5d713dbcad44f3358fec51fd5573d4f733c02cac5a40dcb177787ad5ffe9272f" name = "github.com/json-iterator/go" packages = ["."] + pruneopts = "T" revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" [[projects]] - name = "github.com/konsorten/go-windows-terminal-sequences" + digest = "1:3804a3a02964db8e6db3e5e7960ac1c1a9b12835642dd4f4ac4e56c749ec73eb" + name = "github.com/markbates/inflect" + packages = ["."] + pruneopts = "T" + revision = "24b83195037b3bc61fcda2d28b7b0518bce293b6" + version = "v1.0.4" + +[[projects]] + branch = "master" + digest = "1:fc2b04b0069d6b10bdef96d278fe20c345794009685ed3c8c7f1a6dc023eefec" + name = "github.com/mattbaird/jsonpatch" packages = ["."] - revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" + pruneopts = "T" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + +[[projects]] + digest = "1:a8e3d14801bed585908d130ebfc3b925ba642208e6f30d879437ddfc7bb9b413" + name = "github.com/matttproud/golang_protobuf_extensions" + packages = ["pbutil"] + pruneopts = "T" + revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" version = "v1.0.1" [[projects]] + digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563" name = "github.com/modern-go/concurrent" packages = ["."] + pruneopts = "T" revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" version = "1.0.3" [[projects]] + digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855" name = "github.com/modern-go/reflect2" packages = ["."] + pruneopts = "T" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" version = "1.0.1" [[projects]] + digest = "1:1de2ef6996903caab4bcf41f7885aa276d8e20076ba52ca80b2c6c32efb9c5f5" + name = "github.com/onsi/ginkgo" + packages = [ + ".", + "config", + "internal/codelocation", + "internal/containernode", + "internal/failer", + "internal/leafnodes", + "internal/remote", + "internal/spec", + "internal/spec_iterator", + "internal/specrunner", + "internal/suite", + "internal/testingtproxy", + "internal/writer", + "reporters", + "reporters/stenographer", + "reporters/stenographer/support/go-colorable", + "reporters/stenographer/support/go-isatty", + "types", + ] + pruneopts = "T" + revision = "2e1be8f7d90e9d3e3e58b0ce470f2f14d075406f" + version = "v1.7.0" + +[[projects]] + digest = "1:4f40f5e1925c58d5f9b6a953d1b02f475a6be704e9b5f1a21df6c62c434bde13" + name = "github.com/onsi/gomega" + packages = [ + ".", + "format", + "gbytes", + "gexec", + "internal/assertion", + "internal/asyncassertion", + "internal/oraclematcher", + "internal/testingtsupport", + "matchers", + "matchers/support/goraph/bipartitegraph", + "matchers/support/goraph/edge", + "matchers/support/goraph/node", + "matchers/support/goraph/util", + "types", + ] + pruneopts = "T" + revision = "65fb64232476ad9046e57c26cd0bff3d3a8dc6cd" + version = "v1.4.3" + +[[projects]] + digest = "1:cd3ec56ba4f6993d6a9d7a6459b0cfac0429c01d3963c4afeef5335391e221ca" name = "github.com/open-policy-agent/opa" packages = [ "ast", @@ -87,122 +340,412 @@ "topdown/builtins", "topdown/copypropagation", "types", - "util" + "util", ] - revision = "9fbff4c3302fa82dde5d3cc6ca15be528d77911b" - version = "v0.9.2" + pruneopts = "T" + revision = "6af8da83ad7d5170715ece38d0034ec3984c2786" + version = "v0.10.2" + +[[projects]] + digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9" + name = "github.com/pborman/uuid" + packages = ["."] + pruneopts = "T" + revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" + version = "v1.2" + +[[projects]] + branch = "master" + digest = "1:0c29d499ffc3b9f33e7136444575527d0c3a9463a89b3cbeda0523b737f910b3" + name = "github.com/petar/GoLLRB" + packages = ["llrb"] + pruneopts = "T" + revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" + +[[projects]] + digest = "1:598241bd36d3a5f6d9102a306bd9bf78f3bc253672460d92ac70566157eae648" + name = "github.com/peterbourgon/diskv" + packages = ["."] + pruneopts = "T" + revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" + version = "v2.0.1" [[projects]] + digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" name = "github.com/pkg/errors" packages = ["."] - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" + pruneopts = "T" + revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" + version = "v0.8.1" + +[[projects]] + digest = "1:3b5729e3fc486abc6fc16ce026331c3d196e788c3b973081ecf5d28ae3e1050d" + name = "github.com/prometheus/client_golang" + packages = [ + "prometheus", + "prometheus/internal", + "prometheus/promhttp", + ] + pruneopts = "T" + revision = "505eaef017263e299324067d40ca2c48f6a2cf50" + version = "v0.9.2" [[projects]] branch = "master" + digest = "1:3fa49ea0332c644e4d4ce3864d8eee2cc6c858cb52bf935e32c28d194a0f2bfa" + name = "github.com/prometheus/client_model" + packages = ["go"] + pruneopts = "T" + revision = "f287a105a20ec685d797f65cd0ce8fbeaef42da1" + +[[projects]] + branch = "master" + digest = "1:400226134aab838f5fcfb0cf739c455d62190718067e95c81ea63af5e4d45907" + name = "github.com/prometheus/common" + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model", + ] + pruneopts = "T" + revision = "2998b132700a7d019ff618c06a234b47c1f3f681" + +[[projects]] + branch = "master" + digest = "1:7a4fed4f3801ef4c2f53054230ee75d1f62df4e2a6fab049ca6213c793e386d8" + name = "github.com/prometheus/procfs" + packages = [ + ".", + "internal/util", + "nfs", + "xfs", + ] + pruneopts = "T" + revision = "b1a0a9a36d7453ba0f62578b99712f3a6c5f82d1" + +[[projects]] + branch = "master" + digest = "1:4e1e0e562188a53d5301f2ce0590c1de08ec8820f508fc25cac37ea439e6a914" name = "github.com/rcrowley/go-metrics" packages = ["."] + pruneopts = "T" revision = "3113b8401b8a98917cde58f8bbd42a1b1c03b1fd" [[projects]] + digest = "1:cd3ec56ba4f6993d6a9d7a6459b0cfac0429c01d3963c4afeef5335391e221ca" name = "github.com/rite2nikhil/opa" packages = ["util"] - revision = "9fbff4c3302fa82dde5d3cc6ca15be528d77911b" - version = "v0.9.2" + pruneopts = "T" + revision = "6af8da83ad7d5170715ece38d0034ec3984c2786" + version = "v0.10.2" + +[[projects]] + digest = "1:fac620096bacf6e41b3beb93ff03af9fe545d77421b3e7320f114b2da2e10824" + name = "github.com/rogpeppe/go-internal" + packages = [ + "modfile", + "module", + "semver", + ] + pruneopts = "T" + revision = "d87f08a7d80821c797ffc8eb8f4e01675f378736" + version = "v1.0.0" + +[[projects]] + digest = "1:2b9e473441ec584565880b55467ba1797417f42a60d92af4a761f8485a57e4d0" + name = "github.com/spf13/afero" + packages = [ + ".", + "mem", + ] + pruneopts = "T" + revision = "a5d6946387efe7d64d09dcba68cdd523dc1273a3" + version = "v1.2.0" [[projects]] - name = "github.com/sirupsen/logrus" + digest = "1:8be8b3743fc9795ec21bbd3e0fc28ff6234018e1a269b0a7064184be95ac13e0" + name = "github.com/spf13/cobra" packages = ["."] - revision = "ad15b42461921f1fb3529b058c6786c6a45d5162" - version = "v1.1.1" + pruneopts = "T" + revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" + version = "v0.0.3" [[projects]] + digest = "1:0f775ea7a72e30d5574267692aaa9ff265aafd15214a7ae7db26bc77f2ca04dc" name = "github.com/spf13/pflag" packages = ["."] + pruneopts = "T" revision = "298182f68c66c05229eb03ac171abe6e309ee79a" version = "v1.0.3" [[projects]] branch = "master" + digest = "1:bc2a31092538ed0bdfe9c5c784d6caca3ea5236b690371698826753d2ffa6beb" name = "github.com/yashtewari/glob-intersection" packages = ["."] + pruneopts = "T" revision = "5c77d914dd0ba7bedca923f97232d37137e038f3" +[[projects]] + digest = "1:365b8ecb35a5faf5aa0ee8d798548fc9cd4200cb95d77a5b0b285ac881bae499" + name = "go.uber.org/atomic" + packages = ["."] + pruneopts = "T" + revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" + version = "v1.3.2" + +[[projects]] + digest = "1:8da5d356e645cc806b953f35966e9185e9f3817c6cee54f9b33ca602544ee434" + name = "go.uber.org/multierr" + packages = ["."] + pruneopts = "T" + revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" + version = "v1.1.0" + +[[projects]] + digest = "1:c57d4b95825e74f8eb3b0c851d04aac3626a602de613f8702c0de41de26856b6" + name = "go.uber.org/zap" + packages = [ + ".", + "buffer", + "internal/bufferpool", + "internal/color", + "internal/exit", + "zapcore", + ] + pruneopts = "T" + revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" + version = "v1.9.1" + [[projects]] branch = "master" + digest = "1:5f6a40f74aa9639cfcdaccb526d14a6a7c18900ea8f339416a43a580beb32c34" name = "golang.org/x/crypto" packages = ["ssh/terminal"] - revision = "e84da0312774c21d64ee2317962ef669b27ffb41" + pruneopts = "T" + revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908" [[projects]] branch = "master" + digest = "1:c5b4871a34b4d7b821522983806513e2cf8434a244afc17b86be575f7928ab7c" name = "golang.org/x/net" packages = [ + "context", + "context/ctxhttp", + "html", + "html/atom", + "html/charset", "http/httpguts", "http2", "http2/hpack", - "idna" + "idna", + ] + pruneopts = "T" + revision = "be1c187aa6c66b9daa1d9461c228d17e9dd2cab7" + +[[projects]] + branch = "master" + digest = "1:320e5ba9ea8000060bec710764b8b26c251ee28f6012422b669cb8cb100c9815" + name = "golang.org/x/oauth2" + packages = [ + ".", + "google", + "internal", + "jws", + "jwt", ] - revision = "9b4f9f5ad5197c79fd623a3638e70d8b26cef344" + pruneopts = "T" + revision = "d668ce993890a79bda886613ee587a69dd5da7a6" [[projects]] branch = "master" + digest = "1:13a6901a85d05507bb3d7d956b354369735d31d8cea17e7466cc8975d8dedfda" name = "golang.org/x/sys" packages = [ "unix", - "windows" + "windows", ] - revision = "95b1ffbd15a57cc5abb3f04402b9e8ec0016a52c" + pruneopts = "T" + revision = "48ac38b7c8cbedd50b1613c0fccacfc7d88dfcdf" [[projects]] + digest = "1:6164911cb5e94e8d8d5131d646613ff82c14f5a8ce869de2f6d80d9889df8c5a" name = "golang.org/x/text" packages = [ "collate", "collate/build", + "encoding", + "encoding/charmap", + "encoding/htmlindex", + "encoding/internal", + "encoding/internal/identifier", + "encoding/japanese", + "encoding/korean", + "encoding/simplifiedchinese", + "encoding/traditionalchinese", + "encoding/unicode", "internal/colltab", "internal/gen", "internal/tag", "internal/triegen", "internal/ucd", + "internal/utf8internal", "language", + "runes", "secure/bidirule", "transform", "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "T" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + branch = "master" + digest = "1:077216d94c076b8cd7bd057cb6f7c6d224970cc991bdfe49c0c7a24e8e39ee33" + name = "golang.org/x/time" + packages = ["rate"] + pruneopts = "T" + revision = "85acf8d2951cb2a3bde7632f9ff273ef0379bcbd" + +[[projects]] + branch = "master" + digest = "1:6acf3c464dfdcf9fa948adb7750c9af598aad8ef5f094f91a2344a488db447eb" + name = "golang.org/x/tools" + packages = [ + "go/ast/astutil", + "go/gcexportdata", + "go/internal/cgo", + "go/internal/gcimporter", + "go/internal/packagesdriver", + "go/packages", + "go/types/typeutil", + "imports", + "internal/fastwalk", + "internal/gopathwalk", + "internal/semver", + ] + pruneopts = "T" + revision = "aa033095749b3c5414cb40cb5fb235173be6b186" + +[[projects]] + digest = "1:1469235a5a8e192cfe6a99c4804b883a02f0ff96a693cd1660515a3a3b94d5ac" + name = "google.golang.org/appengine" + packages = [ + ".", + "internal", + "internal/app_identity", + "internal/base", + "internal/datastore", + "internal/log", + "internal/modules", + "internal/remote_api", + "internal/urlfetch", + "urlfetch", + ] + pruneopts = "T" + revision = "e9657d882bb81064595ca3b56cbe2546bbabf7b1" + version = "v1.4.0" + +[[projects]] + digest = "1:7fc160b460a6fc506b37fcca68332464c3f2cd57b6e3f111f26c5bbfd2d5518e" + name = "gopkg.in/fsnotify.v1" + packages = ["."] + pruneopts = "T" + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + source = "https://github.com/fsnotify/fsnotify.git" + version = "v1.4.7" + +[[projects]] + digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a" name = "gopkg.in/inf.v0" packages = ["."] + pruneopts = "T" revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" version = "v0.9.1" [[projects]] + branch = "v1" + digest = "1:0caa92e17bc0b65a98c63e5bc76a9e844cd5e56493f8fdbb28fad101a16254d9" + name = "gopkg.in/tomb.v1" + packages = ["."] + pruneopts = "T" + revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8" + +[[projects]] + digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" name = "gopkg.in/yaml.v2" packages = ["."] - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" + pruneopts = "T" + revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" + version = "v2.2.2" [[projects]] - branch = "release-1.10" + digest = "1:8a1af4b4d5b51e3f8d6ec2a44467e8505e25e29814c2557d1d9717834065c674" name = "k8s.io/api" packages = [ "admission/v1beta1", + "admissionregistration/v1alpha1", + "admissionregistration/v1beta1", + "apps/v1", + "apps/v1beta1", + "apps/v1beta2", "authentication/v1", - "authorization/v1beta1" + "authentication/v1beta1", + "authorization/v1", + "authorization/v1beta1", + "autoscaling/v1", + "autoscaling/v2beta1", + "autoscaling/v2beta2", + "batch/v1", + "batch/v1beta1", + "batch/v2alpha1", + "certificates/v1beta1", + "coordination/v1beta1", + "core/v1", + "events/v1beta1", + "extensions/v1beta1", + "networking/v1", + "policy/v1beta1", + "rbac/v1", + "rbac/v1alpha1", + "rbac/v1beta1", + "scheduling/v1alpha1", + "scheduling/v1beta1", + "settings/v1alpha1", + "storage/v1", + "storage/v1alpha1", + "storage/v1beta1", + ] + pruneopts = "T" + revision = "b503174bad5991eb66f18247f52e41c3258f6348" + version = "kubernetes-1.12.3" + +[[projects]] + digest = "1:3d0ba42a7eaed76c293905ced94201752762c84481f76505ac2079e6f332ae2b" + name = "k8s.io/apiextensions-apiserver" + packages = [ + "pkg/apis/apiextensions", + "pkg/apis/apiextensions/v1beta1", ] - revision = "12444147eb1150aa5c80d2aae532cbc5b7be73d0" + pruneopts = "T" + revision = "0cd23ebeb6882bd1cdc2cb15fc7b2d72e8a86a5b" + version = "kubernetes-1.12.3" [[projects]] - branch = "release-1.10" + digest = "1:bcf693d144a1d98dabdb4ab3bf18fbde8b2f7ee86d2fd8a3f74fb6a035627550" name = "k8s.io/apimachinery" packages = [ + "pkg/api/errors", + "pkg/api/meta", "pkg/api/resource", + "pkg/apis/meta/internalversion", "pkg/apis/meta/v1", + "pkg/apis/meta/v1/unstructured", + "pkg/apis/meta/v1beta1", "pkg/conversion", "pkg/conversion/queryparams", "pkg/fields", @@ -213,28 +756,277 @@ "pkg/runtime/serializer/json", "pkg/runtime/serializer/protobuf", "pkg/runtime/serializer/recognizer", + "pkg/runtime/serializer/streaming", "pkg/runtime/serializer/versioning", "pkg/selection", "pkg/types", + "pkg/util/cache", + "pkg/util/clock", + "pkg/util/diff", "pkg/util/errors", "pkg/util/framer", "pkg/util/intstr", "pkg/util/json", + "pkg/util/mergepatch", + "pkg/util/naming", "pkg/util/net", "pkg/util/runtime", "pkg/util/sets", + "pkg/util/strategicpatch", + "pkg/util/uuid", "pkg/util/validation", "pkg/util/validation/field", "pkg/util/wait", "pkg/util/yaml", + "pkg/version", "pkg/watch", - "third_party/forked/golang/reflect" + "third_party/forked/golang/json", + "third_party/forked/golang/reflect", + ] + pruneopts = "T" + revision = "eddba98df674a16931d2d4ba75edc3a389bf633a" + version = "kubernetes-1.12.3" + +[[projects]] + digest = "1:5aa94390c366adc57a82b19ad4f99482ab8fbeca0a2ff7b09db12d7baf590e46" + name = "k8s.io/client-go" + packages = [ + "discovery", + "dynamic", + "kubernetes", + "kubernetes/scheme", + "kubernetes/typed/admissionregistration/v1alpha1", + "kubernetes/typed/admissionregistration/v1beta1", + "kubernetes/typed/apps/v1", + "kubernetes/typed/apps/v1beta1", + "kubernetes/typed/apps/v1beta2", + "kubernetes/typed/authentication/v1", + "kubernetes/typed/authentication/v1beta1", + "kubernetes/typed/authorization/v1", + "kubernetes/typed/authorization/v1beta1", + "kubernetes/typed/autoscaling/v1", + "kubernetes/typed/autoscaling/v2beta1", + "kubernetes/typed/autoscaling/v2beta2", + "kubernetes/typed/batch/v1", + "kubernetes/typed/batch/v1beta1", + "kubernetes/typed/batch/v2alpha1", + "kubernetes/typed/certificates/v1beta1", + "kubernetes/typed/coordination/v1beta1", + "kubernetes/typed/core/v1", + "kubernetes/typed/events/v1beta1", + "kubernetes/typed/extensions/v1beta1", + "kubernetes/typed/networking/v1", + "kubernetes/typed/policy/v1beta1", + "kubernetes/typed/rbac/v1", + "kubernetes/typed/rbac/v1alpha1", + "kubernetes/typed/rbac/v1beta1", + "kubernetes/typed/scheduling/v1alpha1", + "kubernetes/typed/scheduling/v1beta1", + "kubernetes/typed/settings/v1alpha1", + "kubernetes/typed/storage/v1", + "kubernetes/typed/storage/v1alpha1", + "kubernetes/typed/storage/v1beta1", + "pkg/apis/clientauthentication", + "pkg/apis/clientauthentication/v1alpha1", + "pkg/apis/clientauthentication/v1beta1", + "pkg/version", + "plugin/pkg/client/auth/exec", + "plugin/pkg/client/auth/gcp", + "rest", + "rest/watch", + "restmapper", + "third_party/forked/golang/template", + "tools/auth", + "tools/cache", + "tools/clientcmd", + "tools/clientcmd/api", + "tools/clientcmd/api/latest", + "tools/clientcmd/api/v1", + "tools/leaderelection", + "tools/leaderelection/resourcelock", + "tools/metrics", + "tools/pager", + "tools/record", + "tools/reference", + "transport", + "util/buffer", + "util/cert", + "util/connrotation", + "util/flowcontrol", + "util/homedir", + "util/integer", + "util/jsonpath", + "util/retry", + "util/workqueue", + ] + pruneopts = "T" + revision = "d082d5923d3cc0bfbb066ee5fbdea3d0ca79acf8" + version = "kubernetes-1.12.3" + +[[projects]] + branch = "master" + digest = "1:b11533d0d337c776e3ee4d59f86ca302a66a777b40a289d97cb59a638179f31e" + name = "k8s.io/code-generator" + packages = [ + "cmd/client-gen", + "cmd/client-gen/args", + "cmd/client-gen/generators", + "cmd/client-gen/generators/fake", + "cmd/client-gen/generators/scheme", + "cmd/client-gen/generators/util", + "cmd/client-gen/path", + "cmd/client-gen/types", + "cmd/deepcopy-gen", + "cmd/deepcopy-gen/args", + "pkg/namer", + "pkg/util", + ] + pruneopts = "T" + revision = "3a2206dd6a78497deceb3ae058417fdeb2036c7e" + +[[projects]] + branch = "master" + digest = "1:c5636f57f83eb7ef2a1c7a2340b7e5750f1c105a1d0cbcfcab429d175c216773" + name = "k8s.io/gengo" + packages = [ + "args", + "examples/deepcopy-gen/generators", + "examples/set-gen/sets", + "generator", + "namer", + "parser", + "types", + ] + pruneopts = "T" + revision = "fd15ee9cc2f77baa4f31e59e6acbf21146455073" + +[[projects]] + digest = "1:7a3ef99d492d30157b8e933624a8f0292b4cee5934c23269f7640c8030eb83cd" + name = "k8s.io/klog" + packages = ["."] + pruneopts = "T" + revision = "a5bc97fbc634d635061f3146511332c7e313a55a" + version = "v0.1.0" + +[[projects]] + branch = "master" + digest = "1:90f16a49f856e6d94089444e487c535f4cd41f59a1e90c51deb9dcf965f3c50b" + name = "k8s.io/kube-openapi" + packages = ["pkg/util/proto"] + pruneopts = "T" + revision = "0317810137be915b9cf888946c6e115c1bfac693" + +[[projects]] + digest = "1:0366859202a26d3ad16ff9d7545c20f2a95ced5183f2f3cdb762522dbaffe6ea" + name = "sigs.k8s.io/controller-runtime" + packages = [ + "pkg/cache", + "pkg/cache/internal", + "pkg/client", + "pkg/client/apiutil", + "pkg/client/config", + "pkg/controller", + "pkg/event", + "pkg/handler", + "pkg/internal/controller", + "pkg/internal/controller/metrics", + "pkg/internal/recorder", + "pkg/leaderelection", + "pkg/manager", + "pkg/metrics", + "pkg/patch", + "pkg/predicate", + "pkg/reconcile", + "pkg/recorder", + "pkg/runtime/inject", + "pkg/runtime/log", + "pkg/runtime/signals", + "pkg/source", + "pkg/source/internal", + "pkg/webhook", + "pkg/webhook/admission", + "pkg/webhook/admission/builder", + "pkg/webhook/admission/types", + "pkg/webhook/internal/cert", + "pkg/webhook/internal/cert/generator", + "pkg/webhook/internal/cert/writer", + "pkg/webhook/internal/cert/writer/atomic", + "pkg/webhook/internal/metrics", + "pkg/webhook/types", + ] + pruneopts = "T" + revision = "f6f0bc9611363b43664d08fb097ab13243ef621d" + version = "v0.1.9" + +[[projects]] + digest = "1:f77901562f184eaab833f0b0b6344b0a82b1a26eb9ceb11251cf70d08c1e4423" + name = "sigs.k8s.io/controller-tools" + packages = [ + "cmd/controller-gen", + "pkg/crd/generator", + "pkg/crd/util", + "pkg/internal/codegen", + "pkg/internal/codegen/parse", + "pkg/internal/general", + "pkg/rbac", + "pkg/util", + "pkg/webhook", + "pkg/webhook/internal", + ] + pruneopts = "T" + revision = "950a0e88e4effb864253b3c7504b326cc83b9d11" + version = "v0.1.8" + +[[projects]] + digest = "1:290b4da306982122bcdff12dbababbb95c6e4a94a2995db88baf235ef2f6e93e" + name = "sigs.k8s.io/testing_frameworks" + packages = [ + "integration", + "integration/addr", + "integration/internal", ] - revision = "e386b2658ed20923da8cc9250e552f082899a1ee" + pruneopts = "T" + revision = "d348cb12705b516376e0c323bacca72b00a78425" + version = "v0.1.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "9d6c7c462896a74b9b0387f4e79c6618cb41ebf45141cbb4ea4879352b4686a8" + input-imports = [ + "github.com/emicklei/go-restful", + "github.com/golang/glog", + "github.com/mattbaird/jsonpatch", + "github.com/onsi/ginkgo", + "github.com/onsi/gomega", + "github.com/open-policy-agent/opa/server/types", + "github.com/open-policy-agent/opa/util", + "github.com/rite2nikhil/opa/util", + "k8s.io/api/admission/v1beta1", + "k8s.io/api/admissionregistration/v1beta1", + "k8s.io/api/authentication/v1", + "k8s.io/api/authorization/v1beta1", + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/runtime", + "k8s.io/apimachinery/pkg/runtime/serializer", + "k8s.io/apimachinery/pkg/types", + "k8s.io/client-go/plugin/pkg/client/auth/gcp", + "k8s.io/client-go/rest", + "k8s.io/code-generator/cmd/client-gen", + "k8s.io/code-generator/cmd/deepcopy-gen", + "sigs.k8s.io/controller-runtime/pkg/client/config", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/handler", + "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/runtime/log", + "sigs.k8s.io/controller-runtime/pkg/runtime/signals", + "sigs.k8s.io/controller-runtime/pkg/source", + "sigs.k8s.io/controller-runtime/pkg/webhook", + "sigs.k8s.io/controller-runtime/pkg/webhook/admission", + "sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder", + "sigs.k8s.io/controller-runtime/pkg/webhook/admission/types", + "sigs.k8s.io/controller-tools/cmd/controller-gen", + "sigs.k8s.io/testing_frameworks/integration", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 553eb8cc55f..c056ab0fbcd 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,41 +1,38 @@ -# Gopkg.toml example -# -# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true +required = [ + "github.com/emicklei/go-restful", + "github.com/onsi/ginkgo", # for test framework + "github.com/onsi/gomega", # for test matchers + "k8s.io/client-go/plugin/pkg/client/auth/gcp", # for development against gcp + "k8s.io/code-generator/cmd/client-gen", # for go generate + "k8s.io/code-generator/cmd/deepcopy-gen", # for go generate + "sigs.k8s.io/controller-tools/cmd/controller-gen", # for crd/rbac generation + "sigs.k8s.io/controller-runtime/pkg/client/config", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/handler", + "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/runtime/signals", + "sigs.k8s.io/controller-runtime/pkg/source", + "sigs.k8s.io/testing_frameworks/integration", # for integration testing + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", + ] + +[prune] + go-tests = true -[[constraint]] - name = "github.com/open-policy-agent/opa" - version = "0.9.2" + +# STANZAS BELOW ARE GENERATED AND MAY BE WRITTEN - DO NOT MODIFY BELOW THIS LINE. [[constraint]] - branch = "release-1.10" - name = "k8s.io/api" + name="sigs.k8s.io/controller-runtime" + version="v0.1.1" [[constraint]] - branch = "release-1.10" - name = "k8s.io/apimachinery" + name="sigs.k8s.io/controller-tools" + version="v0.1.1" + +# For dependency below: Refer to issue https://github.com/golang/dep/issues/1799 +[[override]] +name = "gopkg.in/fsnotify.v1" +source = "https://github.com/fsnotify/fsnotify.git" +version="v1.4.7" -[prune] - go-tests = true - unused-packages = true diff --git a/Makefile b/Makefile index d39c266db2e..597fededc4c 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,53 @@ -ORG_PATH=github.com/open-policy-agent -PROJECT_NAME := kubernetes-policy-controller -REPO_PATH="$(ORG_PATH)/$(PROJECT_NAME)" -CONTROLLER_BINARY_NAME := kubernetes-policy-controller -CONTROLLER_VERSION ?= 2.0 -VERSION_VAR := $(REPO_PATH)/version.Version -GIT_VAR := $(REPO_PATH)/version.GitCommit -BUILD_DATE_VAR := $(REPO_PATH)/version.BuildDate -BUILD_DATE := $$(date +%Y-%m-%d-%H:%M) -GIT_HASH := $$(git rev-parse --short HEAD) +# Image URL to use all building/pushing image targets +IMG ?= docker.io/nikhilbh/kubernetes-policy-controller:latest -ifeq ($(OS),Windows_NT) - GO_BUILD_MODE = default -else - UNAME_S := $(shell uname -s) - ifeq ($(UNAME_S), Linux) - GO_BUILD_MODE = pie - endif - ifeq ($(UNAME_S), Darwin) - GO_BUILD_MODE = default - endif -endif +all: test manager -GO_BUILD_OPTIONS := -buildmode=${GO_BUILD_MODE} -ldflags "-s -X $(VERSION_VAR)=$(DEMO_VERSION) -X $(GIT_VAR)=$(GIT_HASH) -X $(BUILD_DATE_VAR)=$(BUILD_DATE)" +# Run tests +test: generate fmt vet manifests + go test ./pkg/... ./cmd/... -coverprofile cover.out -GO_DEBUG_BUILD_OPTIONS := -buildmode=${GO_BUILD_MODE} -gcflags "all=-N -l" -ldflags "-X $(VERSION_VAR)=$(DEMO_VERSION) -X $(GIT_VAR)=$(GIT_HASH) -X $(BUILD_DATE_VAR)=$(BUILD_DATE)" +# Build manager binary +manager: generate fmt vet + go build -o bin/manager github.com/open-policy-agent/kubernetes-policy-controller/cmd/manager -# useful for other docker repos -REGISTRY ?= nikhilbh -CONTROLLER_IMAGE_NAME := $(REGISTRY)/$(CONTROLLER_BINARY_NAME) +# Run against the configured Kubernetes cluster in ~/.kube/config +run: generate fmt vet + go run ./cmd/manager/main.go -clean-controller: - rm -rf bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) +# Install CRDs into a cluster +install: manifests + kubectl apply -f config/crds -clean: - rm -rf bin/$(PROJECT_NAME) +# Deploy controller in the configured Kubernetes cluster in ~/.kube/config +deploy: manifests + # TODO: Re-enable below command once we have crds to deploy + # kubectl apply -f config/crds + kustomize build config/default | kubectl apply -f - -build-controller:clean-controller - go build -o bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) $(GO_BUILD_OPTIONS) $(ORG_PATH)/$(PROJECT_NAME)/cmd +# Generate manifests e.g. CRD, RBAC etc. +manifests: + go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all -build:clean - go build -o bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) $(GO_BUILD_OPTIONS) $(ORG_PATH)/$(PROJECT_NAME)/cmd +# Run go fmt against code +fmt: + go fmt ./pkg/... ./cmd/... -build-dbg-controller:clean-controller - go build -o bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) $(GO_DEBUG_BUILD_OPTIONS) $(ORG_PATH)/$(PROJECT_NAME)/cmd +# Run go vet against code +vet: + go vet ./pkg/... ./cmd/... -build-dbg:clean - go build -o bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) $(GO_DEBUG_BUILD_OPTIONS) $(ORG_PATH)/$(PROJECT_NAME)/cmd +# Generate code +generate: + go generate ./pkg/... ./cmd/... -image-controller: - cp bin/$(PROJECT_NAME)/$(CONTROLLER_BINARY_NAME) images/$(CONTROLLER_BINARY_NAME) - docker build -t $(CONTROLLER_IMAGE_NAME):$(CONTROLLER_VERSION) images/$(CONTROLLER_BINARY_NAME) +# Build the docker image +docker-build: test + docker build . -t ${IMG} + @echo "updating kustomize image patch file for manager resource" + sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml -push-controller: - docker push $(CONTROLLER_IMAGE_NAME):$(CONTROLLER_VERSION) - -.PHONY: build \ No newline at end of file +# Push the docker image +docker-push: + docker push ${IMG} diff --git a/PROJECT b/PROJECT new file mode 100644 index 00000000000..62b75223fcd --- /dev/null +++ b/PROJECT @@ -0,0 +1,3 @@ +version: "1" +domain: styra.org +repo: github.com/open-policy-agent/kubernetes-policy-controller diff --git a/README.md b/README.md index 58fd17f52c0..67a927b898b 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ There are two scenarios of the policy engine namely Validation and Mutation Load the policy as a ConfigMap: ```bash -kubectl create configmap example --from-file ./policy/admission/ingress-host-fqdn.rego +kubectl create configmap example -n kpc-system --from-file ./policy/admission/ingress-host-fqdn.rego ``` ```bash @@ -81,7 +81,7 @@ This policy will mutate resources that define an annotation with the key `"test- Load the policy as a ConfigMap: ```bash -kubectl create configmap example --from-file ./policy/admission/annotate.rego +kubectl create configmap -n kpc-system example2 --from-file ./policy/admission/annotate.rego ``` First create a Deployment: @@ -115,12 +115,13 @@ kubectl get deployment nginx -o json | jq '.metadata' 1. Add the authorization module to the APIServer via flag, e.g.: `--authorization-mode=Node,Webhook,RBAC` 1. Configure a webhook config file which is used by the APIServer to call the webhook, e.g.: `--authorization-webhook-config-file=/etc/kubernetes/kubernetes-policy-controller.kubeconfig`. See example file content [here](./deploy/kubernetes-policy-controller.kubeconfig) 1. Deploy the policy-controller via static pod manifest. Place e.g. the following file in `/etc/kubernetes/manifests/`. See example file content [here](./deploy/kubernetes-policy-controller.yaml). In this case no `kube-mgmt` container is deployed, because this would lead to an circular dependency. In this case the policies are stored in the folder `/etc/kubernetes/policy` on the master node. Alternatively, they could be deployed via shared volume and an `initContainer`. + 1. To avoid dependencies on the Kubernetes API Server use the flag `--authorization-mode=true` 1. Deploy some of the policies stored under [policy/authorization](./policy/authorization). There are examples for: - 1. Blocking create/update/delete on Calico CRDs - 1. Namespace-based blocking of the usage of `privileged` PodSecurityPolicy - 1. Blocking access to StorageClass `cinder` - 1. Blocking create/update/delete on ValidatingWebhookConfigurations & MutatingWebhookConfigurations (which isn't possible via ValidatingWebhooks & MutatingWebhooks) - 1. Blocking `exec` and `cp` on Pods in kube-system + 1. Blocking create/update/delete on Calico CRDs + 1. Namespace-based blocking of the usage of `privileged` PodSecurityPolicy + 1. Blocking access to StorageClass `cinder` + 1. Blocking create/update/delete on ValidatingWebhookConfigurations & MutatingWebhookConfigurations (which isn't possible via ValidatingWebhooks & MutatingWebhooks) + 1. Blocking `exec` and `cp` on Pods in kube-system *Note* This authorization modules are never called for users with group `system:masters` @@ -316,6 +317,36 @@ deny[{ [Demo video of Kubernetes Policy Controller](https://youtu.be/1WObJiTZDHc) +## Dev Workflow + +### Build the Docker Image + +```bash +make docker-build +``` + +### Push the Docker Image + +```bash +make docker-push +``` + +### Deploy + +The following command deploys the latest pushed docker image to the currently active Kubernetes context + +```bash +make deploy +``` + +### Rebuild Config Templates + +The following command rebuilds the config manifests used with `make deploy`: + +```bash +make manifests +``` + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to the CNCF Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index c271109aeb2..00000000000 --- a/cmd/main.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "io/ioutil" - "os" - "regexp" - - "github.com/open-policy-agent/kubernetes-policy-controller/pkg/opa" - "github.com/open-policy-agent/kubernetes-policy-controller/pkg/server" - "github.com/sirupsen/logrus" - flag "github.com/spf13/pflag" -) - -var ( - defaultAddr = ":7925" // default listening address for server -) - -// WhSvrParameters parameters -type parameters struct { - // Addrs are the listening addresses that the OPA server will bind to. - addrs *[]string - // Certificate is the certificate to use in server-mode. If the certificate - // is nil, the server will NOT use TLS. - certificate *tls.Certificate - - logLevel string - - // CAs to use for the communication with OPA, per default the system certs are used - // if the flag opa-ca-file is set the CA certs from this file are also used - opaCAs *x509.CertPool - // Addresse of OPA - opaAddress string - // auth token which is used via Bearer scheme to communicate with OPA - opaAuthToken string -} - -var ( - addrs = flag.StringSlice("addr", []string{defaultAddr}, "set listening address of the server (e.g., [ip]: for TCP)") - tlsCertFile = flag.String("tls-cert-file", "/certs/tls.crt", "set path of TLS certificate file") - tlsPrivateKeyFile = flag.String("tls-private-key-file", "/certs/tls.key", "set path of TLS private key file") - opaAddress = flag.String("opa-url", "http://localhost:8181/v1", "set URL of OPA API endpoint") - opaCAFile = flag.String("opa-ca-file", "", "set path of the ca crts which are used in addition to the system certs to verify the opa server cert") - opaAuthTokenFile = flag.String("opa-auth-token-file", "", "set path of auth token file where the bearer token for opa is stored in format 'token = \"\"'") - logLevel = flag.String("log-level", "info", "log level, which can be: panic, fatal, error, warn, info or debug") -) - -func main() { - // get command line parameters - flag.Parse() - - cert, err := loadCertificate(*tlsCertFile, *tlsPrivateKeyFile) - if err != nil { - fmt.Println("error:", err) - os.Exit(1) - } - - opaAuthToken, err := loadOpaAuthToken(*opaAuthTokenFile) - if err != nil { - fmt.Println("error:", err) - os.Exit(1) - } - - opaCAs, err := loadCACertificates(*opaCAFile) - if err != nil { - fmt.Println("error:", err) - os.Exit(1) - } - - params := parameters{ - addrs: addrs, - certificate: cert, - opaAddress: *opaAddress, - opaCAs: opaCAs, - opaAuthToken: opaAuthToken, - logLevel: *logLevel, - } - - /* - if err := server.InstallDefaultAdmissionPolicy("default-kubernetes-matches", types.KubernetesPolicy, opa.New(params.opaAddress)); err != nil { - logrus.Fatalf("Failed to install default policy: %v", err) - } - if err := server.InstallDefaultAdmissionPolicy("default-policy-matches", types.PolicyMatchPolicy, opa.New(params.opaAddress)); err != nil { - logrus.Fatalf("Failed to install default policy: %v", err) - } - */ - - ctx := context.Background() - StartServer(ctx, params) -} - -// StartServer starts the runtime in server mode. This function will block the calling goroutine. -func StartServer(ctx context.Context, params parameters) { - - loglevel, err := logrus.ParseLevel(params.logLevel) - if err != nil { - logrus.WithField("err", err).Fatalf("Unable to parse log level.") - } - logrus.SetLevel(loglevel) - - logrus.WithFields(logrus.Fields{ - "addrs": params.addrs, - }).Infof("First line of log stream.") - - s, err := server.New(). - WithAddresses(*params.addrs). - WithCertificate(params.certificate). - WithOPA(opa.New(params.opaAddress, params.opaCAs, params.opaAuthToken)). - Init(ctx) - - if err != nil { - logrus.WithField("err", err).Fatalf("Unable to initialize server.") - } - - loops, err := s.Listeners() - if err != nil { - logrus.WithField("err", err).Fatalf("Unable to create listeners.") - } - - errc := make(chan error) - for _, loop := range loops { - go func(serverLoop func() error) { - errc <- serverLoop() - }(loop) - } - for { - select { - case err := <-errc: - logrus.WithField("err", err).Fatal("Listener failed.") - } - } -} - -func loadCertificate(tlsCertFile, tlsPrivateKeyFile string) (*tls.Certificate, error) { - - if tlsCertFile != "" && tlsPrivateKeyFile != "" { - cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsPrivateKeyFile) - if err != nil { - return nil, err - } - return &cert, nil - } else if tlsCertFile != "" || tlsPrivateKeyFile != "" { - return nil, fmt.Errorf("--tls-cert-file and --tls-private-key-file must be specified together") - } - - return nil, nil -} - -func loadCACertificates(tlsCertFile string) (*x509.CertPool, error) { - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } - - if tlsCertFile != "" { - certs, err := ioutil.ReadFile(tlsCertFile) - if err != nil { - return nil, fmt.Errorf("could not load ca certificate: %v", err) - } - - // Append our cert to the system pool - if ok := rootCAs.AppendCertsFromPEM(certs); !ok { - logrus.Info("No certs appended, using system certs only") - } - } - - return rootCAs, nil -} - -var opaAuthTokenRegex = regexp.MustCompile("token.*=.*\"(.*)\"") - -func loadOpaAuthToken(opaAuthTokenFile string) (string, error) { - if opaAuthTokenFile == "" { - return "", nil - } - - bytes, err := ioutil.ReadFile(opaAuthTokenFile) - if err != nil { - return "", fmt.Errorf("error reading opaAuthTokenFile: %v", err) - } - - match := opaAuthTokenRegex.FindStringSubmatch(string(bytes)) - if len(match) != 2 { - return "", fmt.Errorf("error matching token in opaAuthTokenFile, matched: %v", match) - } - - return match[1], nil -} diff --git a/cmd/manager/main.go b/cmd/manager/main.go new file mode 100644 index 00000000000..0078f9ed94b --- /dev/null +++ b/cmd/manager/main.go @@ -0,0 +1,93 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "os" + + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/apis" + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/controller" + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/standalone" + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/webhook" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/runtime/signals" +) + +var authorizationMode = flag.Bool("authorization-mode", false, "Use barebones functionality, avoiding dependencies on the Kubernetes API. This allows the binary to serve as a standalone authorization server.") + +func main() { + flag.Parse() + logf.SetLogger(logf.ZapLogger(false)) + log := logf.Log.WithName("entrypoint") + + // Ultimately authorization mode should likely be a separate binary + if !*authorizationMode { + // Get a config to talk to the apiserver + log.Info("setting up client for manager") + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "unable to set up client config") + os.Exit(1) + } + + // Create a new Cmd to provide shared dependencies and start components + log.Info("setting up manager") + mgr, err := manager.New(cfg, manager.Options{}) + if err != nil { + log.Error(err, "unable to set up overall controller manager") + os.Exit(1) + } + + log.Info("Registering Components.") + + // Setup Scheme for all resources + log.Info("setting up scheme") + if err := apis.AddToScheme(mgr.GetScheme()); err != nil { + log.Error(err, "unable add APIs to scheme") + os.Exit(1) + } + + // Setup all Controllers + log.Info("Setting up controller") + if err := controller.AddToManager(mgr); err != nil { + log.Error(err, "unable to register controllers to the manager") + os.Exit(1) + } + + log.Info("setting up webhooks") + if err := webhook.AddToManager(mgr); err != nil { + log.Error(err, "unable to register webhooks to the manager") + os.Exit(1) + } + + // Start the Cmd + log.Info("Starting the Cmd.") + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + log.Error(err, "unable to run the manager") + os.Exit(1) + } + } else { + log.Info("launching in authorizaton mode") + if err := standalone.Serve(); err != nil { + log.Error(err, "unable to run the manager") + os.Exit(1) + } + } +} diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml new file mode 100644 index 00000000000..7f2a1a41f71 --- /dev/null +++ b/config/default/kustomization.yaml @@ -0,0 +1,33 @@ +# Adds namespace to all resources. +namespace: kpc-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: kpc- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +# Each entry in this list must resolve to an existing +# resource definition in YAML. These are the resource +# files that kustomize reads, modifies and emits as a +# YAML string, with resources separated by document +# markers ("---"). +resources: +- ../rbac/rbac_role.yaml +- ../rbac/rbac_role_binding.yaml +- ../manager/manager.yaml + +patches: +- manager_image_patch.yaml + +vars: +- name: WEBHOOK_SECRET_NAME + objref: + kind: Secret + name: webhook-server-secret + apiVersion: v1 diff --git a/config/default/manager_image_patch.yaml b/config/default/manager_image_patch.yaml new file mode 100644 index 00000000000..8b344fbffc5 --- /dev/null +++ b/config/default/manager_image_patch.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + # Change the value of image field below to your controller image URL + - image: docker.io/nikhilbh/kubernetes-policy-controller:latest + name: manager diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 00000000000..588b48face7 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,99 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + controller-tools.k8s.io: "1.0" + name: system +--- +apiVersion: v1 +kind: Service +metadata: + name: controller-manager-service + namespace: system + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" +spec: + selector: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + ports: + - port: 443 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" +spec: + selector: + matchLabels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + serviceName: controller-manager-service + template: + metadata: + labels: + control-plane: controller-manager + controller-tools.k8s.io: "1.0" + spec: + containers: + - command: + - /root/manager +# args: +# - "--stderrthreshold=INFO" + image: controller:latest + imagePullPolicy: Always + name: manager + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SECRET_NAME + value: $(WEBHOOK_SECRET_NAME) + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + ports: + - containerPort: 9876 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /certs + name: cert + readOnly: true + - name: opa + image: openpolicyagent/opa:0.10.1 + imagePullPolicy: Always + args: + - "run" + - "--server" + - "--addr=http://127.0.0.1:8181" + - name: kube-mgmt + image: openpolicyagent/kube-mgmt:0.6 + imagePullPolicy: Always + args: + - "--replicate-cluster=v1/namespaces" + - "--replicate=extensions/v1beta1/ingresses" + - "--replicate=v1/pods" + - "--policies=kpc-system" + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-secret + namespace: system diff --git a/config/rbac/rbac_role.yaml b/config/rbac/rbac_role.yaml new file mode 100644 index 00000000000..63a010eea13 --- /dev/null +++ b/config/rbac/rbac_role.yaml @@ -0,0 +1,63 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - '*' + resources: + - '*' + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + - validatingwebhookconfigurations + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - create + - update + - patch + - delete diff --git a/config/rbac/rbac_role_binding.yaml b/config/rbac/rbac_role_binding.yaml new file mode 100644 index 00000000000..c1033e23fb9 --- /dev/null +++ b/config/rbac/rbac_role_binding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + creationTimestamp: null + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/demo/port-forward.sh b/demo/port-forward.sh index ac196cc070f..48d425f9c21 100755 --- a/demo/port-forward.sh +++ b/demo/port-forward.sh @@ -1,4 +1,4 @@ #!/bin/bash -controllerpod=$(kubectl -n opa get po --no-headers | awk '{print $1}') -kubectl -n opa port-forward $controllerpod 7925:7925 \ No newline at end of file +controllerpod=$(kubectl -n kpc-system get po --no-headers | awk '{print $1}') +kubectl -n kpc-system port-forward $controllerpod 7925:7925 \ No newline at end of file diff --git a/deploy/createnamespace.sh b/deploy/createnamespace.sh index e3d5f27f9ab..67b83e66486 100755 --- a/deploy/createnamespace.sh +++ b/deploy/createnamespace.sh @@ -3,9 +3,9 @@ cd "${0%/*}" set -e -echo "Create opa namespace" +echo "Create kpc-system namespace" read -p "Press enter to continue" # create opa namespace -kubectl create ns opa \ No newline at end of file +kubectl create ns kpc-system \ No newline at end of file diff --git a/deploy/createsecret.sh b/deploy/createsecret.sh deleted file mode 100755 index 16129016f48..00000000000 --- a/deploy/createsecret.sh +++ /dev/null @@ -1,33 +0,0 @@ - -#!/bin/bash - -cd "${0%/*}" - -set -e -echo "Create opa-server TLS secret.Communication between Kubernetes and OPA must be secured using TLS. To configure TLS" - -read -p "Press enter to continue" - -rm -rf ./secret -mkdir ./secret -cd ./secret - -openssl genrsa -out ca.key 2048 -openssl req -x509 -new -nodes -key ca.key -days 100000 -out ca.crt -subj "/CN=admission_ca" -cat >server.conf <server.conf < ./secret/mutating-webhook-configuration.yaml < ./secret/mutating-webhook-configuration.yaml <\"'") +) + +func NewFromFlags() Client { + opaAuthToken, err := loadOpaAuthToken(*opaAuthTokenFile) + if err != nil { + fmt.Println("error:", err) + os.Exit(1) + } + + opaCAs, err := loadCACertificates(*opaCAFile) + if err != nil { + fmt.Println("error:", err) + os.Exit(1) + } + + return New(*opaAddress, opaCAs, opaAuthToken) +} + +func loadCACertificates(tlsCertFile string) (*x509.CertPool, error) { + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + if tlsCertFile != "" { + certs, err := ioutil.ReadFile(tlsCertFile) + if err != nil { + return nil, fmt.Errorf("could not load ca certificate: %v", err) + } + + // Append our cert to the system pool + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + glog.Info("No certs appended, using system certs only") + } + } + + return rootCAs, nil +} + +var opaAuthTokenRegex = regexp.MustCompile("token.*=.*\"(.*)\"") + +func loadOpaAuthToken(opaAuthTokenFile string) (string, error) { + if opaAuthTokenFile == "" { + return "", nil + } + + bytes, err := ioutil.ReadFile(opaAuthTokenFile) + if err != nil { + return "", fmt.Errorf("error reading opaAuthTokenFile: %v", err) + } + + match := opaAuthTokenRegex.FindStringSubmatch(string(bytes)) + if len(match) != 2 { + return "", fmt.Errorf("error matching token in opaAuthTokenFile, matched: %v", match) + } + + return match[1], nil +} + // Error contains the standard error fields returned by OPA. type Error struct { Code string `json:"code"` diff --git a/pkg/opa/opa_test.go b/pkg/opa/opa_test.go index 77e9344dfa7..658c55161a5 100644 --- a/pkg/opa/opa_test.go +++ b/pkg/opa/opa_test.go @@ -65,7 +65,7 @@ func TestHTTPClientMakePatch(t *testing.T) { for _, tc := range tests { - client := &httpClient{"URL", tc.prefix} + client := &httpClient{url: "URL", prefix: tc.prefix} var value *interface{} if tc.value != "" { diff --git a/pkg/standalone/server.go b/pkg/standalone/server.go new file mode 100644 index 00000000000..c4e010db90c --- /dev/null +++ b/pkg/standalone/server.go @@ -0,0 +1,147 @@ +package standalone + +import ( + "crypto/tls" + "flag" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/opa" + "github.com/open-policy-agent/kubernetes-policy-controller/pkg/webhook" +) + +var ( + addrs = flag.String("addr", ":7925", "set listening address of the server (e.g., [ip]: for TCP)") + tlsCertFile = flag.String("tls-cert-file", "/certs/tls.crt", "set path of TLS certificate file. Only works in authorization mode.") + tlsPrivateKeyFile = flag.String("tls-private-key-file", "/certs/tls.key", "set path of TLS private key file. Only works in authorization mode.") +) + +// Serve launches a standalone server that does not depend on the Kubernetes API Server. +func Serve() error { + srv := &Server{ + sMux: http.NewServeMux(), + } + opa := opa.NewFromFlags() + webhook.AddGenericWebhooks(srv, opa) + srv.addrs = strings.Split(*addrs, ",") + + cert, err := loadCertificate(*tlsCertFile, *tlsPrivateKeyFile) + if err != nil { + return err + } + srv.cert = cert + + loops, err := srv.Listeners() + if err != nil { + return err + } + + errc := make(chan error) + for _, loop := range loops { + go func(serverLoop func() error) { + errc <- serverLoop() + }(loop) + } + for { + select { + case err := <-errc: + return err + } + } +} + +var _ webhook.GenericHandler = &Server{} + +type Server struct { + sMux *http.ServeMux + cert *tls.Certificate + addrs []string +} + +// Handle registers an http handler with the server at the provided path. +func (s *Server) Handle(path string, h http.Handler) { + s.sMux.Handle(path, h) +} + +// Loop will contain all the calls from the server that we'll be listening on. +type Loop func() error + +// Listeners returns functions that listen and serve connections. +func (s *Server) Listeners() ([]Loop, error) { + loops := []Loop{} + for _, addr := range s.addrs { + parsedURL, err := parseURL(addr, s.cert != nil) + if err != nil { + return nil, err + } + var loop Loop + switch parsedURL.Scheme { + case "http": + loop, err = s.getListenerForHTTPServer(parsedURL) + case "https": + loop, err = s.getListenerForHTTPSServer(parsedURL) + default: + err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme) + } + if err != nil { + return nil, err + } + loops = append(loops, loop) + } + + return loops, nil +} + +func (s *Server) getListenerForHTTPServer(u *url.URL) (Loop, error) { + httpServer := http.Server{ + Addr: u.Host, + Handler: s.sMux, + } + httpLoop := func() error { return httpServer.ListenAndServe() } + + return httpLoop, nil +} + +func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, error) { + if s.cert == nil { + return nil, fmt.Errorf("TLS certificate required but not supplied") + } + httpsServer := http.Server{ + Addr: u.Host, + Handler: s.sMux, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{*s.cert}, + }, + } + httpsLoop := func() error { return httpsServer.ListenAndServeTLS("", "") } + + return httpsLoop, nil +} + +func parseURL(s string, useHTTPSByDefault bool) (*url.URL, error) { + if !strings.Contains(s, "://") { + scheme := "http://" + if useHTTPSByDefault { + scheme = "https://" + } + s = scheme + s + } + return url.Parse(s) +} + +func loadCertificate(tlsCertFile, tlsPrivateKeyFile string) (*tls.Certificate, error) { + + if tlsCertFile != "" && tlsPrivateKeyFile != "" { + cert, err := tls.LoadX509KeyPair(tlsCertFile, tlsPrivateKeyFile) + if err != nil { + return nil, err + } + return &cert, nil + } else if tlsCertFile != "" || tlsPrivateKeyFile != "" { + return nil, fmt.Errorf("--tls-cert-file and --tls-private-key-file must be specified together") + } + + return nil, nil +} diff --git a/pkg/server/server.go b/pkg/webhook/policy.go similarity index 55% rename from pkg/server/server.go rename to pkg/webhook/policy.go index f308a9fb016..fb72940e27e 100644 --- a/pkg/server/server.go +++ b/pkg/webhook/policy.go @@ -1,8 +1,7 @@ -package server +package webhook import ( "context" - "crypto/tls" "encoding/json" "errors" "fmt" @@ -10,191 +9,145 @@ import ( "math/rand" "net" "net/http" - "net/url" "runtime" "runtime/debug" "strings" "time" - "github.com/gorilla/mux" + "github.com/golang/glog" + "github.com/mattbaird/jsonpatch" "github.com/open-policy-agent/kubernetes-policy-controller/pkg/opa" "github.com/open-policy-agent/kubernetes-policy-controller/pkg/policies/types" "github.com/open-policy-agent/opa/util" - log "github.com/sirupsen/logrus" - "k8s.io/api/admission/v1beta1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" authenticationv1 "k8s.io/api/authentication/v1" authorizationv1beta1 "k8s.io/api/authorization/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" + apitypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder" + atypes "sigs.k8s.io/controller-runtime/pkg/webhook/admission/types" ) +func init() { + AddToManagerFuncs = append(AddToManagerFuncs, AddPolicyWebhook) +} + var ( runtimeScheme = k8sruntime.NewScheme() codecs = serializer.NewCodecFactory(runtimeScheme) deserializer = codecs.UniversalDeserializer() ) -// Server defines the server for the Webhook -type Server struct { - Handler http.Handler - - router *mux.Router - addrs []string - cert *tls.Certificate - Opa opa.Query -} - -// Loop will contain all the calls from the server that we'll be listening on. -type Loop func() error - -// New returns a new Server. -func New() *Server { - s := Server{} - return &s -} - -// Init initializes the server. This function MUST be called before Loop. -func (s *Server) Init(ctx context.Context) (*Server, error) { - s.initRouter() - - return s, nil -} +// AddPolicyWebhook registers the policy webhook server with the manager +// below: notations add permissions kube-mgmt needs. Access cannot yet be restricted on a namespace-level granularity +// +kubebuilder:rbac:groups=*,resources=*,verbs=get;list;watch +// +kubebuilder:rbac:groups=,resources=configmaps,verbs=get;list;watch;create;update;patch;delete +func AddPolicyWebhook(mgr manager.Manager) error { + opa := opa.NewFromFlags() + + serverOptions := webhook.ServerOptions{ + CertDir: "/certs", + BootstrapOptions: &webhook.BootstrapOptions{ + MutatingWebhookConfigName: "kpc", + Secret: &apitypes.NamespacedName{ + Namespace: "kpc-system", + Name: "kpc-webhook-server-secret", + }, + Service: &webhook.Service{ + Namespace: "kpc-system", + Name: "kpc-controller-manager-service", + }, + }, + } -// WithAddresses sets the listening addresses that the server will bind to. -func (s *Server) WithAddresses(addrs []string) *Server { - s.addrs = addrs - return s + s, err := webhook.NewServer("policy-admission-server", mgr, serverOptions) + if err != nil { + return err + } + return addWebhooks(mgr, s, opa) } -// WithCertificate sets the server-side certificate that the server will use. -func (s *Server) WithCertificate(cert *tls.Certificate) *Server { - s.cert = cert - return s +// GenericHandler is any object that supports the webhook server's Handle() method. +type GenericHandler interface { + Handle(string, http.Handler) } -// WithOPA sets the opa client that the server will use. -func (s *Server) WithOPA(opa opa.Query) *Server { - s.Opa = opa - return s +// serverLike is a shim to enable easy testing. This interface supports both endpoint registration +// methods of Kubebuilder's webhook server. +type serverLike interface { + GenericHandler + Register(...webhook.Webhook) error } -// Listeners returns functions that listen and serve connections. -func (s *Server) Listeners() ([]Loop, error) { - loops := []Loop{} - for _, addr := range s.addrs { - parsedURL, err := parseURL(addr, s.cert != nil) - if err != nil { - return nil, err - } - var loop Loop - switch parsedURL.Scheme { - case "http": - loop, err = s.getListenerForHTTPServer(parsedURL) - case "https": - loop, err = s.getListenerForHTTPSServer(parsedURL) - default: - err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme) - } - if err != nil { - return nil, err - } - loops = append(loops, loop) - } - - return loops, nil -} +// addWebhooks adds all webhooks to the provided server-like object. +func addWebhooks(mgr manager.Manager, server serverLike, opa opa.Query) error { + AddGenericWebhooks(server, opa) -func (s *Server) getListenerForHTTPServer(u *url.URL) (Loop, error) { - httpServer := http.Server{ - Addr: u.Host, - Handler: s.Handler, + mutatingWh, err := builder.NewWebhookBuilder(). + Mutating(). + Name("mutation.styra.com"). + Path("/v1/admit"). + Rules(admissionregistrationv1beta1.RuleWithOperations{ + Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update}, + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"*"}, + }, + }). + Handlers(mutationHandler{opa: opa}). + WithManager(mgr). + Build() + if err != nil { + return err } - httpLoop := func() error { return httpServer.ListenAndServe() } - return httpLoop, nil -} - -func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, error) { - if s.cert == nil { - return nil, fmt.Errorf("TLS certificate required but not supplied") - } - httpsServer := http.Server{ - Addr: u.Host, - Handler: s.Handler, - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{*s.cert}, - }, + if err := server.Register(mutatingWh); err != nil { + return err } - httpsLoop := func() error { return httpsServer.ListenAndServeTLS("", "") } - return httpsLoop, nil + return nil } -func (s *Server) initRouter() { - router := s.router - if router == nil { - router = mux.NewRouter() - } - - router.UseEncodedPath() - router.StrictSlash(true) - - s.registerHandler(router, 1, "/admit", http.MethodPost, appHandler(s.Admit)) - s.registerHandler(router, 1, "/authorize", http.MethodPost, appHandler(s.Authorize)) - s.registerHandler(router, 1, "/audit", http.MethodGet, appHandler(s.Audit)) - - // default 405 +// AddGenericWebhooks adds all handlers that handle raw HTTP requests. +func AddGenericWebhooks(server GenericHandler, opa opa.Query) { + auditWh := newGenericWebhook("/v1/audit", &auditHandler{opa: opa}, []string{http.MethodGet}) + authWh := newGenericWebhook("/v1/authorize", &authorizeHandler{opa: opa}, []string{http.MethodPost}) - router.Handle("/admit/{path:.*}", appHandler(HTTPStatus(405))).Methods(http.MethodHead, http.MethodConnect, http.MethodDelete, - http.MethodGet, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch) - router.Handle("/admit", appHandler(HTTPStatus(405))).Methods(http.MethodHead, - http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodOptions, http.MethodTrace, http.MethodPut, http.MethodPatch) - // default 405 - router.Handle("/authorize/{path:.*}", appHandler(HTTPStatus(405))).Methods(http.MethodHead, http.MethodConnect, http.MethodDelete, - http.MethodGet, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch) - router.Handle("/authorize", appHandler(HTTPStatus(405))).Methods(http.MethodHead, - http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodOptions, http.MethodTrace, http.MethodPut, http.MethodPatch) - // default 405 - router.Handle("/audit/{path:.*}", appHandler(HTTPStatus(405))).Methods(http.MethodHead, http.MethodConnect, http.MethodDelete, - http.MethodGet, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch) - router.Handle("/audit", appHandler(HTTPStatus(405))).Methods(http.MethodHead, - http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch) - - s.Handler = router + server.Handle(auditWh.path, auditWh) + server.Handle(authWh.path, authWh) } -// HTTPStatus is used to set a specific status code -// Adapted from https://stackoverflow.com/questions/27711154/what-response-code-to-return-on-a-non-supported-http-method-on-rest -func HTTPStatus(code int) func(logger *log.Entry, w http.ResponseWriter, req *http.Request) { - return func(logger *log.Entry, w http.ResponseWriter, req *http.Request) { - w.WriteHeader(code) - } -} +// Audit method for reporting current policy complaince of the cluster +var _ genericHandler = &auditHandler{} -func (s *Server) registerHandler(router *mux.Router, version int, path string, method string, handler http.Handler) { - prefix := fmt.Sprintf("/v%d", version) - router.Handle(prefix+path, handler).Methods(method) +type auditHandler struct { + opa opa.Query } -// Audit method for reporting current policy complaince of the cluster -func (s *Server) Audit(logger *log.Entry, w http.ResponseWriter, r *http.Request) { - auditResponse, err := s.audit(logger) +func (h *auditHandler) Handle(w http.ResponseWriter, r *http.Request) { + auditResponse, err := h.audit() if err != nil { - logger.Errorf("error geting audit response: %v", err) + glog.Errorf("error geting audit response: %v", err) http.Error(w, fmt.Sprintf("error gettinf audit response: %v", err), http.StatusInternalServerError) } - logger.Debugf("audit: ready to write reponse %v...", auditResponse) + glog.Infof("audit: ready to write reponse %v...", string(auditResponse)) if _, err := w.Write(auditResponse); err != nil { - logger.Errorf("Can't write response: %v", err) + glog.Infof("Can't write response: %v", err) http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } } // main validation process -func (s *Server) audit(logger *log.Entry) ([]byte, error) { +func (h *auditHandler) audit() ([]byte, error) { validationQuery := types.MakeAuditQuery() - result, err := s.Opa.PostQuery(validationQuery) + result, err := h.opa.PostQuery(validationQuery) if err != nil && !opa.IsUndefinedErr(err) { return nil, err } @@ -210,119 +163,71 @@ func (s *Server) audit(logger *log.Entry) ([]byte, error) { response.Message = fmt.Sprintf("total violations:%v", len(response.Violations)) bs, err = json.Marshal(response) if err != nil { - panic(err) + return nil, err } return bs, nil } -// Admit method for validation and mutation webhook server -func (s *Server) Admit(logger *log.Entry, w http.ResponseWriter, r *http.Request) { - var body []byte - if r.Body != nil { - if data, err := ioutil.ReadAll(r.Body); err == nil { - body = data - } - } - if len(body) == 0 { - logger.Error("empty body") - http.Error(w, "empty body", http.StatusBadRequest) - return - } - // verify the content type is accurate - contentType := r.Header.Get("Content-Type") - if contentType != "application/json" { - logger.Errorf("Content-Type=%s, expect application/json", contentType) - http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) - return - } - var admissionResponse *v1beta1.AdmissionResponse - ar := v1beta1.AdmissionReview{} - deserializer := codecs.UniversalDeserializer() - if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { - logger.Errorf("Can't decode body: %v", err) - admissionResponse = &v1beta1.AdmissionResponse{ - Result: &metav1.Status{ - Message: err.Error(), - }, - } - } else { - admissionResponse = s.admissionPolicyCheck(logger, &ar) - } - admissionReview := v1beta1.AdmissionReview{} - if admissionResponse != nil { - admissionReview.Response = admissionResponse - if ar.Request != nil { - admissionReview.Response.UID = ar.Request.UID - } - } - resp, err := json.Marshal(admissionReview) - if err != nil { - logger.Errorf("Can't encode response: %v", err) - http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) - } - logger.Debugf("Write response %v(allowed: %v)...", admissionReview.Response.UID, admissionReview.Response.Allowed) - if _, err := w.Write(resp); err != nil { - logger.Errorf("Can't write response: %v", err) - http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) - } +// Handler for the mutation admission webhook +var _ admission.Handler = mutationHandler{} + +type mutationHandler struct { + opa opa.Query } -// main admission process -func (s *Server) admissionPolicyCheck(logger *log.Entry, ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { - response := &v1beta1.AdmissionResponse{ - Allowed: true, - } - if ar.Request == nil { - logger.Errorf("AdmissionReview request is nil, +%v", *ar) - return response - } - req := ar.Request - logger.Debugf("AdmissionReview for Resource=%v Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", req.Resource, req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo) +func (h mutationHandler) Handle(ctx context.Context, req atypes.Request) atypes.Response { + ar := req.AdmissionRequest + glog.Infof("AdmissionReview for Resource=%v Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", ar.Resource, ar.Kind, ar.Namespace, ar.Name, ar.UID, ar.Operation, ar.UserInfo) // do admission policy check - allowed, reason, patchBytes, err := s.doAdmissionPolicyCheck(logger, req) + allowed, reason, patchBytes, err := h.doAdmissionPolicyCheck(ar) + + // Note we are allowing access on an erroring policy check if err != nil { - logger.Debugf("policy check failed Resource=%v Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v ar=%+v error=%v", req.Resource, req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo, ar, err) - return response + msg := fmt.Sprintf("policy check failed Resource=%v Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v ar=%+v error=%v", ar.Resource, ar.Kind, ar.Namespace, ar.Name, ar.UID, ar.Operation, ar.UserInfo, req, err) + glog.Infof(msg) + return admission.ValidationResponse(true, msg) } + if patchBytes == nil || len(patchBytes) == 0 { - logger.Debugf("AdmissionResponse: No mutation due to policy check, Resource=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", req.Resource.Resource, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo) - return &v1beta1.AdmissionResponse{ - Allowed: allowed, + glog.Infof("AdmissionResponse: No mutation due to policy check, Resource=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", ar.Resource.Resource, ar.Namespace, ar.Name, ar.UID, ar.Operation, ar.UserInfo) + return admission.ValidationResponse(allowed, reason) + } + + glog.Infof("AdmissionResponse: Mutate Resource=%v Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", ar.Resource.Resource, ar.Namespace, ar.Name, ar.UID, ar.Operation, ar.UserInfo) + patches := []jsonpatch.JsonPatchOperation{} + if err := json.Unmarshal(patchBytes, &patches); err != nil { + msg := fmt.Sprintf("poorly formed JSONPatch Resource=%v Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v ar=%+v error=%v", ar.Resource, ar.Kind, ar.Namespace, ar.Name, ar.UID, ar.Operation, ar.UserInfo, req, err) + glog.Infof(msg) + return admission.ValidationResponse(true, msg) + } + return atypes.Response{ + Patches: patches, + Response: &admissionv1beta1.AdmissionResponse{ + Allowed: true, Result: &metav1.Status{ Message: reason, }, - } - } - logger.Debugf("AdmissionResponse: Mutate Resource=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v", req.Resource.Resource, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo) - return &v1beta1.AdmissionResponse{ - Allowed: true, - Patch: patchBytes, - Result: &metav1.Status{ - Message: reason, + PatchType: func() *admissionv1beta1.PatchType { pt := admissionv1beta1.PatchTypeJSONPatch; return &pt }(), }, - PatchType: func() *v1beta1.PatchType { - pt := v1beta1.PatchTypeJSONPatch - return &pt - }(), } } -func (s *Server) doAdmissionPolicyCheck(logger *log.Entry, req *v1beta1.AdmissionRequest) (allowed bool, reason string, patchBytes []byte, err error) { +func (h *mutationHandler) doAdmissionPolicyCheck(req *admissionv1beta1.AdmissionRequest) (allowed bool, reason string, patchBytes []byte, err error) { var mutationQuery string if mutationQuery, err = makeOPAAdmissionPostQuery(req); err != nil { return false, "", nil, err } - logger.Debugf("Sending admission query to opa: %v", mutationQuery) + glog.Infof("Sending admission query to opa: %v", mutationQuery) - result, err := s.Opa.PostQuery(mutationQuery) + result, err := h.opa.PostQuery(mutationQuery) if err != nil && !opa.IsUndefinedErr(err) { return false, "", nil, fmt.Errorf("opa query failed query=%s err=%v", mutationQuery, err) } - logger.Debugf("Response from admission query to opa: %v", result) + glog.Infof("Response from admission query to opa: %v", result) return createPatchFromOPAResult(result) } @@ -389,7 +294,7 @@ func makeOPAWithAsQuery(query, path, value string) string { return fmt.Sprintf(`%s with %s as %s`, query, path, value) } -func makeOPAAdmissionPostQuery(req *v1beta1.AdmissionRequest) (string, error) { +func makeOPAAdmissionPostQuery(req *admissionv1beta1.AdmissionRequest) (string, error) { var resource, name string if resource = strings.ToLower(strings.TrimSpace(req.Resource.Resource)); len(resource) == 0 { return resource, fmt.Errorf("resource is empty") @@ -434,7 +339,7 @@ type admissionRequest struct { OldObject json.RawMessage `json:"oldObject,omitempty" protobuf:"bytes,10,opt,name=oldObject"` } -func createAdmissionRequestValueForOPA(req *v1beta1.AdmissionRequest) (string, error) { +func createAdmissionRequestValueForOPA(req *admissionv1beta1.AdmissionRequest) (string, error) { ar := admissionRequest{ UID: string(req.UID), Kind: req.Kind, @@ -455,7 +360,13 @@ func createAdmissionRequestValueForOPA(req *v1beta1.AdmissionRequest) (string, e } // Authorize method for authorization module webhook server -func (s *Server) Authorize(logger *log.Entry, w http.ResponseWriter, r *http.Request) { +var _ genericHandler = &authorizeHandler{} + +type authorizeHandler struct { + opa opa.Query +} + +func (h *authorizeHandler) Handle(w http.ResponseWriter, r *http.Request) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { @@ -463,21 +374,21 @@ func (s *Server) Authorize(logger *log.Entry, w http.ResponseWriter, r *http.Req } } if len(body) == 0 { - logger.Error("empty body") + glog.Error("empty body") http.Error(w, "empty body", http.StatusBadRequest) return } // verify the content type is accurate contentType := r.Header.Get("Content-Type") if contentType != "application/json" { - logger.Errorf("Content-Type=%s, expect application/json", contentType) + glog.Errorf("Content-Type=%s, expect application/json", contentType) http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) return } sar := authorizationv1beta1.SubjectAccessReview{} deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(body, nil, &sar); err != nil { - logger.Errorf("Can't decode body: %v", err) + glog.Errorf("Can't decode body %v: %v", string(body), err) sar = authorizationv1beta1.SubjectAccessReview{ TypeMeta: metav1.TypeMeta{ APIVersion: "authorization.k8s.io/v1beta1", @@ -490,36 +401,36 @@ func (s *Server) Authorize(logger *log.Entry, w http.ResponseWriter, r *http.Req }, } } else { - sar.Status = s.authorizationPolicyCheck(logger, &sar) + sar.Status = h.authorizationPolicyCheck(&sar) } resp, err := json.Marshal(sar) if err != nil { - logger.Errorf("Can't encode response: %v", err) + glog.Errorf("Can't encode response: %v", err) http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) } - logger.Debugf("SubjectAccessResponse: (denied: %v)\n", sar.Status.Denied) + glog.Infof("SubjectAccessResponse: (denied: %v)\n", sar.Status.Denied) if _, err := w.Write(resp); err != nil { - logger.Errorf("Can't write response: %v", err) + glog.Errorf("Can't write response: %v", err) http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } } // main authorization process -func (s *Server) authorizationPolicyCheck(logger *log.Entry, sar *authorizationv1beta1.SubjectAccessReview) authorizationv1beta1.SubjectAccessReviewStatus { +func (h *authorizeHandler) authorizationPolicyCheck(sar *authorizationv1beta1.SubjectAccessReview) authorizationv1beta1.SubjectAccessReviewStatus { response := authorizationv1beta1.SubjectAccessReviewStatus{ Allowed: false, Denied: true, } attributes := sarAttributes(sar) - logger.Debugf("SubjectAccessReview for %s", attributes) + glog.Infof("SubjectAccessReview for %s", attributes) // do authorization policy check - allowed, reason, err := s.doAuthorizationPolicyCheck(logger, sar) + allowed, reason, err := h.doAuthorizationPolicyCheck(sar) if err != nil { - logger.Debugf("policy check failed %s ar=%+v error=%v", attributes, sar, err) + glog.Infof("policy check failed %s ar=%+v error=%v", attributes, sar, err) return response } - logger.Debugf("SubjectAccessReviewStatus: denied: %t reason: %s %s", !allowed, reason, attributes) + glog.Infof("SubjectAccessReviewStatus: denied: %t reason: %s %s", !allowed, reason, attributes) return authorizationv1beta1.SubjectAccessReviewStatus{ Allowed: false, Denied: !allowed, @@ -536,21 +447,21 @@ func sarAttributes(sar *authorizationv1beta1.SubjectAccessReview) string { return strings.Join(attrs, " ") } -func (s *Server) doAuthorizationPolicyCheck(logger *log.Entry, sar *authorizationv1beta1.SubjectAccessReview) (allowed bool, reason string, err error) { +func (h *authorizeHandler) doAuthorizationPolicyCheck(sar *authorizationv1beta1.SubjectAccessReview) (allowed bool, reason string, err error) { var authorizationQuery string if authorizationQuery, err = makeOPAAuthorizationPostQuery(sar); err != nil { return false, "", err } - logger.Debugf("Sending authorization query to opa: %v", authorizationQuery) + glog.Infof("Sending authorization query to opa: %v", authorizationQuery) - result, err := s.Opa.PostQuery(authorizationQuery) + result, err := h.opa.PostQuery(authorizationQuery) if err != nil && !opa.IsUndefinedErr(err) { return false, fmt.Sprintf("opa query failed query=%s err=%v", authorizationQuery, err), err } - logger.Debugf("Response from authorization query to opa: %v", result) + glog.Infof("Response from authorization query to opa: %v", result) return parseOPAResult(result) } @@ -655,21 +566,7 @@ func randStringBytesMaskImprSrc(n int) string { return string(b) } -// InstallDefaultAdmissionPolicy will update OPA with a default policy This function will -// block until the policy has been installed. -func InstallDefaultAdmissionPolicy(id string, policy []byte, opa opa.Policies) error { - for { - time.Sleep(time.Second * 1) - if err := opa.InsertPolicy(id, policy); err != nil { - log.Errorf("Failed to install default policy (kubernetesPolicy) : %v", err) - } else { - return nil - } - } -} - -type appHandler func(*log.Entry, http.ResponseWriter, *http.Request) - +// Generic HTTP methods and structs type responseWriter struct { http.ResponseWriter statusCode int @@ -684,14 +581,35 @@ func newResponseWriter(w http.ResponseWriter) *responseWriter { return &responseWriter{w, http.StatusOK} } +var _ http.Handler = GenericWebhook{} + +type genericHandler interface { + Handle(http.ResponseWriter, *http.Request) +} + +type GenericWebhook struct { + handler genericHandler + path string + methods []string +} + +func newGenericWebhook(path string, handler genericHandler, methods []string) GenericWebhook { + return GenericWebhook{ + path: path, + methods: methods, + handler: handler, + } +} + // ServeHTTP implements the net/http server handler interface // and recovers from panics. -func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - logger := log.WithFields(log.Fields{ - "req.method": r.Method, - "req.path": r.URL.Path, - "req.remote": parseRemoteAddr(r.RemoteAddr), - }) +func (gw GenericWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { + glog.Infof("Received request: method=%v, path=%v, remote=%v", r.Method, r.URL.Path, parseRemoteAddr(r.RemoteAddr)) + if !gw.allowedMethod(r.Method) || !gw.allowedPath(r.URL.Path) { + w.WriteHeader(405) + return + } + start := time.Now() defer func() { var err error @@ -706,15 +624,31 @@ func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: err = errors.New("unknown error") } - logger.WithField("res.status", http.StatusInternalServerError). - Errorf("Panic processing request: %+v, file: %s, line: %d, stacktrace: '%s'", r, file, line, stack) + glog.Errorf("Panic processing request: %+v, file: %s, line: %d, stacktrace: '%s'", r, file, line, stack) http.Error(w, err.Error(), http.StatusInternalServerError) } }() rw := newResponseWriter(w) - fn(logger, rw, r) + gw.handler.Handle(rw, r) latency := time.Since(start) - logger.Infof("Status (%d) took %d ns", rw.statusCode, latency.Nanoseconds()) + glog.Infof("Status (%d) took %d ns", rw.statusCode, latency.Nanoseconds()) +} + +func (gw GenericWebhook) allowedMethod(method string) bool { + for _, m := range gw.methods { + if m == method { + return true + } + } + return false +} + +func (gw GenericWebhook) allowedPath(path string) bool { + extra := strings.Replace(path, gw.path, "", 1) + if extra != "" && extra != "/" { + return false + } + return true } func parseRemoteAddr(addr string) string { @@ -728,14 +662,3 @@ func parseRemoteAddr(addr string) string { } return hostname } - -func parseURL(s string, useHTTPSByDefault bool) (*url.URL, error) { - if !strings.Contains(s, "://") { - scheme := "http://" - if useHTTPSByDefault { - scheme = "https://" - } - s = scheme + s - } - return url.Parse(s) -} diff --git a/pkg/server/server_test.go b/pkg/webhook/policy_test.go similarity index 69% rename from pkg/server/server_test.go rename to pkg/webhook/policy_test.go index b1dcb30d3da..983ffed76c5 100644 --- a/pkg/server/server_test.go +++ b/pkg/webhook/policy_test.go @@ -1,7 +1,6 @@ -package server +package webhook import ( - "context" "encoding/base64" "encoding/json" "fmt" @@ -12,175 +11,179 @@ import ( "strings" "testing" - "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/open-policy-agent/kubernetes-policy-controller/pkg/opa" "github.com/open-policy-agent/kubernetes-policy-controller/pkg/policies/types" opatypes "github.com/open-policy-agent/opa/server/types" "github.com/open-policy-agent/opa/util" "k8s.io/api/admission/v1beta1" authorizationv1beta1 "k8s.io/api/authorization/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" ) func TestAuditWithValidateViolation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(``, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) setup := []tr{ - {http.MethodGet, "/audit", "", 200, `{"message": "total violations:1","violations": [{"id": "anyID","resolution": {"message": "anyMessage"},"resource": {"kind": "anyKind","name": "anyName","namespace": "anyNamespace"}}]}`}, + {"violation", http.MethodGet, "/audit", "", 200, `{"message": "total violations:1","violations": [{"id": "anyID","resolution": {"message": "anyMessage"},"resource": {"kind": "anyKind","name": "anyName","namespace": "anyNamespace"}}]}`}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestSingleValidation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(`anyname.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) violationRequest := makeAdmissionRequest("anyKind", "anyName", "anyNamespace") validRequest := makeAdmissionRequest("anyKind", "validName", "validNamespace") setup := []tr{ - {http.MethodPost, "/admit", violationRequest, 200, `{"response": {"allowed": false,"status": {"message": "[\"anyMessage\"]","metadata": {}},"uid": "anyUID"}}`}, - {http.MethodPost, "/admit", validRequest, 200, `{"response": {"allowed": true, "status": {"message": "valid based on configured policies", "metadata": {}}, "uid": "anyUID"}}`}, + {"violation", http.MethodPost, "/admit", violationRequest, 200, `{"response": {"allowed": false,"status": {"reason": "[\"anyMessage\"]","metadata": {},"code": 200},"uid": "anyUID"}}`}, + // "W10=" is the base64 encoding of "[]" + {"valid", http.MethodPost, "/admit", validRequest, 200, `{"response": {"allowed": true, "patch": "W10=", "patchType": "JSONPatch", "status": {"metadata": {},"code": 200}, "uid": "anyUID"}}`}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestMultipleValidation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(`anyname.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage1", nil), opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage2", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) violationRequest := makeAdmissionRequest("anyKind", "anyName", "anyNamespace") setup := []tr{ - {http.MethodPost, "/admit", violationRequest, 200, `{"response": {"allowed": false,"status": {"message": "[\"anyMessage1\",\"anyMessage2\"]","metadata": {}},"uid": "anyUID"}}`}, + {"violation", http.MethodPost, "/admit", violationRequest, 200, `{"response": {"allowed": false,"status": {"reason": "[\"anyMessage1\",\"anyMessage2\"]","metadata": {},"code": 200},"uid": "anyUID"}}`}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestSingleMutation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} patches := []types.PatchOperation{{Op: "anyOp", Path: "anyPath", Value: "anyValue"}} fakeOpa.SetViolation(`anyname.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", patches)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) expectedPatchBytes, err := getPatchBytes(patches) if err != nil { t.Fatal(err) } - expectedRespBody := fmt.Sprintf(`{"response": {"allowed": true, "patch": "%s", "patchType": "JSONPatch", "status": {"message": "applying patches", "metadata": {}}, "uid": "anyUID"}}`, string(expectedPatchBytes)) + expectedRespBody := fmt.Sprintf(`{"response": {"allowed": true, "patch": "W10=", "patchType": "JSONPatch", "patch": "%s", "patchType": "JSONPatch", "status": {"metadata": {},"code": 200}, "uid": "anyUID"}}`, string(expectedPatchBytes)) mutationRequest := makeAdmissionRequest("anyKind", "anyName", "anyNamespace") setup := []tr{ - {http.MethodPost, "/admit", mutationRequest, 200, expectedRespBody}, + {"mutation", http.MethodPost, "/admit", mutationRequest, 200, expectedRespBody}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestMultipleNonConflictingMutation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} + // TODO: Flaky, order of patches not guaranteed patches := []types.PatchOperation{{Op: "anyOp", Path: "anyPath1", Value: "anyValue"}, {Op: "anyOp", Path: "anyPath2", Value: "anyValue"}} fakeOpa.SetViolation(`anyname.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", patches)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) expectedPatchBytes, err := getPatchBytes(patches) if err != nil { t.Fatal(err) } - expectedRespBody := fmt.Sprintf(`{"response": {"allowed": true, "patch": "%s", "patchType": "JSONPatch", "status": {"message": "applying patches", "metadata": {}}, "uid": "anyUID"}}`, string(expectedPatchBytes)) + expectedRespBody := fmt.Sprintf(`{"response": {"allowed": true, "patch": "W10=", "patchType": "JSONPatch", "patch": "%s", "patchType": "JSONPatch", "status": {"metadata": {},"code": 200}, "uid": "anyUID"}}`, string(expectedPatchBytes)) mutationRequest := makeAdmissionRequest("anyKind", "anyName", "anyNamespace") setup := []tr{ - {http.MethodPost, "/admit", mutationRequest, 200, expectedRespBody}, + {"mutation", http.MethodPost, "/admit", mutationRequest, 200, expectedRespBody}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestMultipleConflictingMutation(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} patches := []types.PatchOperation{{Op: "anyOp", Path: "anyPath", Value: "anyValue"}, {Op: "anyOp", Path: "anyPath", Value: "anyValue"}} fakeOpa.SetViolation(`anyname.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", patches)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) mutationRequest := makeAdmissionRequest("anyKind", "anyName", "anyNamespace") setup := []tr{ - {http.MethodPost, "/admit", mutationRequest, 200, `{"response": {"allowed": false, "status": {"message": "conflicting patches caused denied request, operations ({Op:anyOp Path:anyPath Value:anyValue}, {Op:anyOp Path:anyPath Value:anyValue})","metadata": {}},"uid": "anyUID"}}`}, + {"mutation", http.MethodPost, "/admit", mutationRequest, 200, `{"response": {"allowed": false, "status": {"reason": "conflicting patches caused denied request, operations ({Op:anyOp Path:anyPath Value:anyValue}, {Op:anyOp Path:anyPath Value:anyValue})","metadata": {},"code": 200},"uid": "anyUID"}}`}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } @@ -251,20 +254,18 @@ func TestPatchResultEmpty(t *testing.T) { } func TestSingleAuthorization(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(`apps.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) violationRequest := makeSubjectAccessReview("apiregistration.k8s.io", "v1beta1", "apiservices", "", "create", "", "v1.apps", "admin", []string{"system:authenticated"}) validRequest := makeSubjectAccessReview("apiregstration.k8s.io", "v1beta1", "apiservices", "", "create", "", "custom.metrics.k8s.io", "admin", []string{"system:authenticated"}) setup := []tr{ - {http.MethodPost, "/authorize", violationRequest, 200, ` { + {"violation", http.MethodPost, "/authorize", violationRequest, 200, ` { "apiVersion": "authorization.k8s.io/v1beta1", "kind": "SubjectAccessReview", "metadata": { @@ -289,7 +290,7 @@ func TestSingleAuthorization(t *testing.T) { "reason": "[\"anyMessage\"]" } }`}, - {http.MethodPost, "/authorize", validRequest, 200, ` { + {"valid", http.MethodPost, "/authorize", validRequest, 200, ` { "apiVersion": "authorization.k8s.io/v1beta1", "kind": "SubjectAccessReview", "metadata": { @@ -315,30 +316,30 @@ func TestSingleAuthorization(t *testing.T) { } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestSingleAuthorizationWithUnparseableSubjectAccessReview(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(`apps.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) violationRequest1 := `{"broken SubjectAccessReview"}` violationRequest2 := `` setup := []tr{ - {http.MethodPost, "/authorize", violationRequest1, 200, ` { + {"violation", http.MethodPost, "/authorize", violationRequest1, 200, ` { "apiVersion": "authorization.k8s.io/v1beta1", "kind": "SubjectAccessReview", "metadata": { @@ -351,34 +352,34 @@ func TestSingleAuthorizationWithUnparseableSubjectAccessReview(t *testing.T) { "evaluationError": "couldn't get version/kind; json parse error: invalid character '}' after object key" } }`}, - {http.MethodPost, "/authorize", violationRequest2, 400, ""}, + {"violation2", http.MethodPost, "/authorize", violationRequest2, 400, ""}, } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } func TestMultipleAuthorization(t *testing.T) { - f := newFixture(t) + f := newFixture() fakeOpa := &opa.FakeOPA{} fakeOpa.SetViolation(`apps.*`, opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage", nil), opa.MakeDenyObject("anyID", "anyKind", "anyName", "anyNamespace", "anyMessage2", nil)) - f.server, _ = New(). - WithAddresses([]string{":8182"}). - WithOPA(fakeOpa). - Init(context.Background()) + mgr, _ := manager.New(&rest.Config{}, manager.Options{}) + addWebhooks(mgr, f.server, fakeOpa) violationRequest := makeSubjectAccessReview("apiregistration.k8s.io", "v1beta1", "apiservices", "", "create", "", "v1.apps", "admin", []string{"system:authenticated"}) setup := []tr{ - {http.MethodPost, "/authorize", violationRequest, 200, ` { + {"violation", http.MethodPost, "/authorize", violationRequest, 200, ` { "apiVersion": "authorization.k8s.io/v1beta1", "kind": "SubjectAccessReview", "metadata": { @@ -406,12 +407,14 @@ func TestMultipleAuthorization(t *testing.T) { } for _, tr := range setup { - req := newReqV1(tr.method, tr.path, tr.body) - req.RemoteAddr = "testaddr" + t.Run(tr.name, func(t *testing.T) { + req := newReqV1(tr.method, tr.path, tr.body) + req.RemoteAddr = "testaddr" - if err := f.executeRequest(req, tr.code, tr.resp); err != nil { - t.Fatal(err) - } + if err := f.executeRequest(req, tr.code, tr.resp); err != nil { + t.Fatal(err) + } + }) } } @@ -522,26 +525,36 @@ func makeSubjectAccessReview(group, version, resource, subResource, verb, namesp return string(b) } +var _ serverLike = &testServer{} + +// testServer emulates the registry/routing of webhook.Server +// Ideally this should be replaced with code that relies on the behavior of webhook.Server directly. +type testServer struct { + sMux *http.ServeMux +} + +func (s *testServer) Handle(path string, handler http.Handler) { + s.sMux.Handle(path, handler) +} + +func (s *testServer) Register(webhooks ...webhook.Webhook) error { + for _, wh := range webhooks { + s.sMux.Handle(wh.GetPath(), wh.Handler()) + } + return nil +} + type fixture struct { - server *Server + server *testServer recorder *httptest.ResponseRecorder - t *testing.T } -func newFixture(t *testing.T) *fixture { - ctx := context.Background() - server, err := New(). - WithAddresses([]string{":7925"}). - Init(ctx) - if err != nil { - panic(err) - } +func newFixture() *fixture { recorder := httptest.NewRecorder() return &fixture{ - server: server, + server: &testServer{sMux: http.NewServeMux()}, recorder: recorder, - t: t, } } @@ -569,7 +582,7 @@ func newReqUnversioned(method, path, body string) *http.Request { func (f *fixture) executeRequest(req *http.Request, code int, resp string) error { f.reset() - f.server.Handler.ServeHTTP(f.recorder, req) + f.server.sMux.ServeHTTP(f.recorder, req) if f.recorder.Code != code { return fmt.Errorf("Expected code %v from %v %v but got: %v", code, req.Method, req.URL, f.recorder) } @@ -598,6 +611,7 @@ func (f *fixture) executeRequest(req *http.Request, code int, resp string) error } type tr struct { + name string method string path string body string diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go new file mode 100644 index 00000000000..4cb65871ced --- /dev/null +++ b/pkg/webhook/webhook.go @@ -0,0 +1,36 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// AddToManagerFuncs is a list of functions to add all Controllers to the Manager +var AddToManagerFuncs []func(manager.Manager) error + +// AddToManager adds all Controllers to the Manager +// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations;validatingwebhookconfigurations,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete +func AddToManager(m manager.Manager) error { + for _, f := range AddToManagerFuncs { + if err := f(m); err != nil { + return err + } + } + return nil +}