Skip to content

Commit

Permalink
Refactor cosigned to take advantage of duck typing. (#637)
Browse files Browse the repository at this point in the history
* Refactor cosigned to take advantage of duck typing.

With this change, the webhook can take advantage of duck typing to parse all of the "Pod Specable" types currently supported.

This also takes advantage of the `knative.dev/pkg` webhook infrastructure to reduce boilerplate and eliminate the need for `cert-manager`.

Lastly, this starts to sketch out some cosigned e2e tests to verify that things work.

Signed-off-by: Matt Moore <mattomata@gmail.com>

* Make port configurable, pull tests out into a script.

Signed-off-by: Matt Moore <mattomata@gmail.com>

* Drop GO111MODULE, drop v1beta1 admission review, improve flag desc, hoist and comment webhook name as constant

Signed-off-by: Matt Moore <mattomata@gmail.com>
  • Loading branch information
mattmoor committed Sep 11, 2021
1 parent 739947d commit fb04df8
Show file tree
Hide file tree
Showing 26 changed files with 1,091 additions and 450 deletions.
183 changes: 183 additions & 0 deletions .github/workflows/kind-e2e-cosigned.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Cosigned KinD E2E

on:
pull_request:
branches: [ 'main', 'release-*' ]

defaults:
run:
shell: bash
working-directory: ./src/github.com/sigstore/cosign

jobs:

e2e-tests:
name: e2e tests
runs-on: ubuntu-latest
strategy:
fail-fast: false # Keep running if one leg fails.
matrix:
k8s-version:
- v1.19.11
- v1.20.7
- v1.21.1

include:
# Map between K8s and KinD versions.
# This is attempting to make it a bit clearer what's being tested.
# See: https://github.com/kubernetes-sigs/kind/releases
- k8s-version: v1.19.11
kind-version: v0.11.1
kind-image-sha: sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729
cluster-suffix: c${{ github.run_id }}.local
- k8s-version: v1.20.7
kind-version: v0.11.1
kind-image-sha: sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9
cluster-suffix: c${{ github.run_id }}.local
- k8s-version: v1.21.1
kind-version: v0.11.1
kind-image-sha: sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6
cluster-suffix: c${{ github.run_id }}.local

env:
GOPATH: ${{ github.workspace }}
# https://github.com/google/go-containerregistry/pull/125 allows insecure registry for
# '*.local' hostnames.
REGISTRY_NAME: registry.local
REGISTRY_PORT: 5000
KO_DOCKER_REPO: registry.local:5000/cosigned

steps:
- name: Set up Go 1.16.x
uses: actions/setup-go@v2
with:
go-version: 1.16.x

- name: Install Dependencies
working-directory: ./
run: |
echo '::group:: install ko'
curl -L https://github.com/google/ko/releases/download/v0.8.3/ko_0.8.3_Linux_x86_64.tar.gz | tar xzf - ko
chmod +x ./ko
sudo mv ko /usr/local/bin
echo '::endgroup::'
- name: Check out code onto GOPATH
uses: actions/checkout@v2
with:
path: ./src/github.com/sigstore/cosign

- name: Install Cosign
run: |
go install ./cmd/cosign
# This KinD setup is based on what we use for knative/serving on GHA, and it includes several "fun"
# monkey wrenches (e.g. randomizing cluster suffix: `.svc.cluster.local`) to make sure we don't bake
# in any invalid assumptions about a particular Kubernetes configuration.
- name: Install KinD
run: |
set -x
# Disable swap otherwise memory enforcement doesn't work
# See: https://kubernetes.slack.com/archives/CEKK1KTN2/p1600009955324200
sudo swapoff -a
sudo rm -f /swapfile
# Use in-memory storage to avoid etcd server timeouts.
# https://kubernetes.slack.com/archives/CEKK1KTN2/p1615134111016300
# https://github.com/kubernetes-sigs/kind/issues/845
sudo mkdir -p /tmp/etcd
sudo mount -t tmpfs tmpfs /tmp/etcd
curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/${{ matrix.kind-version }}/kind-$(uname)-amd64
chmod +x ./kind
sudo mv kind /usr/local/bin
- name: Configure KinD Cluster
run: |
set -x
# KinD configuration.
cat > kind.yaml <<EOF
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
# Configure registry for KinD.
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."$REGISTRY_NAME:$REGISTRY_PORT"]
endpoint = ["http://$REGISTRY_NAME:$REGISTRY_PORT"]
# This is needed in order to support projected volumes with service account tokens.
# See: https://kubernetes.slack.com/archives/CEKK1KTN2/p1600268272383600
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
metadata:
name: config
apiServer:
extraArgs:
"service-account-issuer": "kubernetes.default.svc"
"service-account-signing-key-file": "/etc/kubernetes/pki/sa.key"
networking:
dnsDomain: "${{ matrix.cluster-suffix }}"
nodes:
- role: control-plane
image: kindest/node:${{ matrix.k8s-version }}@${{ matrix.kind-image-sha }}
extraMounts:
- containerPath: /var/lib/etcd
hostPath: /tmp/etcd
- role: worker
image: kindest/node:${{ matrix.k8s-version }}@${{ matrix.kind-image-sha }}
EOF
- name: Create KinD Cluster
run: |
set -x
kind create cluster --config kind.yaml
- name: Setup local registry
run: |
# Run a registry.
docker run -d --restart=always \
-p $REGISTRY_PORT:$REGISTRY_PORT --name $REGISTRY_NAME registry:2
# Connect the registry to the KinD network.
docker network connect "kind" $REGISTRY_NAME
# Make the $REGISTRY_NAME -> 127.0.0.1, to tell `ko` to publish to
# local reigstry, even when pushing $REGISTRY_NAME:$REGISTRY_PORT/some/image
sudo echo "127.0.0.1 $REGISTRY_NAME" | sudo tee -a /etc/hosts
- name: Install cosigned
run: |
ko apply -Bf config/
# Update the cosign verification-key secret with a proper key pair.
cosign generate-key-pair k8s://cosign-system/verification-key
# Wait for the webhook to come up and become Ready
kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook
- name: Run Tests
run: |
./test/e2e_test_cosigned.sh
- name: Collect diagnostics
if: ${{ failure() }}
run: |
# Add more namespaces to dump here.
for ns in cosign-system; do
kubectl get pods -n${ns}
echo '::group:: describe'
kubectl describe pods -n${ns}
echo '::endgroup::'
for x in $(kubectl get pods -n${ns} -oname); do
echo "::group:: describe $x"
kubectl describe -n${ns} $x
echo '::endgroup::'
echo "::group:: $x logs"
kubectl logs -n${ns} $x --all-containers
echo '::endgroup::'
done
done
32 changes: 32 additions & 0 deletions cmd/cosign/webhook/depcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Copyright 2021 The Sigstore Authors.
//
// 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_test

import (
"testing"

"knative.dev/pkg/depcheck"
)

func TestNoDeps(t *testing.T) {
depcheck.AssertNoDependency(t, map[string][]string{
"github.com/sigstore/cosign/cmd/cosign/webhook": {
// This conflicts with klog, we error on startup about
// `-log_dir` being defined multiple times.
"github.com/golang/glog",
},
})
}
1 change: 1 addition & 0 deletions cmd/cosign/webhook/kodata/HEAD
1 change: 1 addition & 0 deletions cmd/cosign/webhook/kodata/LICENSE
1 change: 1 addition & 0 deletions cmd/cosign/webhook/kodata/refs
136 changes: 60 additions & 76 deletions cmd/cosign/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,99 +17,83 @@ package main

import (
"context"
goflag "flag"
"net"
"net/http"
"os"
"strconv"
"flag"

flag "github.com/spf13/pflag"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook"
duckv1 "knative.dev/pkg/apis/duck/v1"
"knative.dev/pkg/configmap"
"knative.dev/pkg/controller"
"knative.dev/pkg/injection/sharedmain"
"knative.dev/pkg/signals"
"knative.dev/pkg/webhook"
"knative.dev/pkg/webhook/certificates"
"knative.dev/pkg/webhook/resourcesemantics"
"knative.dev/pkg/webhook/resourcesemantics/validation"

cwebhook "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook"
)

func main() {
ctrl.SetLogger(zap.New(func(o *zap.Options) {
o.Development = true
}))

var (
metricsAddr = net.ParseIP("127.0.0.1")
metricsPort uint16 = 8080
var secretName = flag.String("secret-name", "", "The name of the secret in the webhook's namespace that holds the public key for verification.")

bindAddr = net.ParseIP("0.0.0.0")
bindPort uint16 = 8443
// webhookName holds the name of the validating webhook to set up with the
// types we are watching. If this changes, you must also change:
// ./config/500-webhook-configuration.yaml
const webhookName = "cosigned.sigstore.dev"

tlsCertDirectory string
secretKeyRef string
)

klog.InitFlags(goflag.CommandLine)
flags := flag.NewFlagSet("main", flag.ExitOnError)
flags.AddGoFlagSet(goflag.CommandLine)
flags.StringVar(&secretKeyRef, "secret-key-ref", "", "The secret that includes pub/private key pair")
flags.IPVar(&metricsAddr, "metrics-address", metricsAddr, "The address the metric endpoint binds to.")
flags.Uint16Var(&metricsPort, "metrics-port", metricsPort, "The port the metric endpoint binds to.")
flags.IPVar(&bindAddr, "bind-address", bindAddr, ""+
"The IP address on which to listen for the --secure-port port.")
flags.Uint16Var(&bindPort, "secure-port", bindPort, "The port on which to serve HTTPS.")
flags.StringVar(&tlsCertDirectory, "tls-cert-dir", tlsCertDirectory, "The directory where the TLS certs are located.")

err := flags.Parse(os.Args[1:])
if err != nil {
klog.Error(err)
os.Exit(1)
func main() {
opts := webhook.Options{
ServiceName: "webhook",
Port: 8443,
SecretName: "webhook-certs",
}
ctx := webhook.WithOptions(signals.NewContext(), opts)

cosignedValidationFuncs := map[schema.GroupVersionKind]webhook.ValidationFunc{
corev1.SchemeGroupVersion.WithKind("Pod"): webhook.ValidateSignedResources,
batchv1.SchemeGroupVersion.WithKind("Job"): webhook.ValidateSignedResources,
appsv1.SchemeGroupVersion.WithKind("Deployment"): webhook.ValidateSignedResources,
appsv1.SchemeGroupVersion.WithKind("StatefulSet"): webhook.ValidateSignedResources,
appsv1.SchemeGroupVersion.WithKind("ReplicateSet"): webhook.ValidateSignedResources,
appsv1.SchemeGroupVersion.WithKind("DaemonSet"): webhook.ValidateSignedResources,
}
// Allow folks to configure the port the webhook serves on.
flag.IntVar(&opts.Port, "secure-port", opts.Port, "The port on which to serve HTTPS.")

cosignedValidationHook := webhook.NewFuncAdmissionValidator(webhook.Scheme, cosignedValidationFuncs, secretKeyRef)
// This calls flag.Parse()
sharedmain.MainWithContext(ctx, "cosigned",
certificates.NewController,
NewValidatingAdmissionController,
)
}

opts := ctrl.Options{
Scheme: webhook.Scheme,
MetricsBindAddress: net.JoinHostPort(metricsAddr.String(), strconv.Itoa(int(metricsPort))),
Host: bindAddr.String(),
Port: int(bindPort),
CertDir: tlsCertDirectory,
}
func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
validator := cwebhook.NewValidator(ctx, *secretName)

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opts)
if err != nil {
klog.Error(err, "Failed to create manager")
os.Exit(1)
}
return validation.NewAdmissionController(ctx,
// Name of the resource webhook.
webhookName,

// Get the controller manager webhook server.
webhookServer := mgr.GetWebhookServer()
// The path on which to serve the webhook.
"/validations",

// Register the webhooks in the server.
webhookServer.Register("/validations", cosignedValidationHook)
// The resources to validate.
map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
corev1.SchemeGroupVersion.WithKind("Pod"): &duckv1.Pod{},

// Add healthz and readyz handlers to webhook server. The controller-runtime AddHealthzCheck/AddReadyzCheck methods
// are served via separate http server - better to serve these from the same webhook http server.
webhookServer.WebhookMux.Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{}))
webhookServer.WebhookMux.Handle("/healthz/", http.StripPrefix("/healthz/", &healthz.Handler{}))
appsv1.SchemeGroupVersion.WithKind("ReplicaSet"): &duckv1.WithPod{},
appsv1.SchemeGroupVersion.WithKind("Deployment"): &duckv1.WithPod{},
appsv1.SchemeGroupVersion.WithKind("StatefulSet"): &duckv1.WithPod{},
appsv1.SchemeGroupVersion.WithKind("DaemonSet"): &duckv1.WithPod{},
batchv1.SchemeGroupVersion.WithKind("Job"): &duckv1.WithPod{},
},

klog.Info("Starting the webhook...")
// A function that infuses the context passed to Validate/SetDefaults with custom metadata.
func(ctx context.Context) context.Context {
ctx = duckv1.WithPodValidator(ctx, validator.ValidatePod)
ctx = duckv1.WithPodSpecValidator(ctx, validator.ValidatePodSpecable)
return ctx
},

// Start the server by starting a previously-set-up manager
if err := mgr.Start(context.Background()); err != nil {
klog.Error(err)
os.Exit(1)
}
// Whether to disallow unknown fields.
// We pass false because we're using partial schemas.
false,

// Extra validating callbacks to be applied to resources.
nil,
)
}

0 comments on commit fb04df8

Please sign in to comment.