diff --git a/.github/workflows/kind-cluster-custom-resources.yaml b/.github/workflows/kind-cluster-custom-resources.yaml new file mode 100644 index 000000000..dcdfd59c6 --- /dev/null +++ b/.github/workflows/kind-cluster-custom-resources.yaml @@ -0,0 +1,148 @@ +# Copyright 2022 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. + +name: Test policy-controller with custom resource + +on: + pull_request: + branches: [ 'main', 'release-*' ] + +defaults: + run: + shell: bash + +permissions: read-all + +jobs: + cip-test-trustroot-bring-your-own-keys: + name: ClusterImagePolicy e2e tests with custom resources + runs-on: ubuntu-latest + + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + k8s-version: + - v1.27.x + - v1.28.x + - v1.29.x + + env: + KO_DOCKER_REPO: "registry.local:5000/policy-controller" + SCAFFOLDING_RELEASE_VERSION: "v0.7.2" + GO111MODULE: on + GOFLAGS: -ldflags=-s -ldflags=-w + KOCACHE: ~/ko + + steps: + - name: free up disk space for the release + run: | + rm -rf /usr/share/dotnet/ + rm -rf "$AGENT_TOOLSDIRECTORY" + rm -rf "/usr/local/share/boost" + rm -rf /opt/ghc + docker rmi $(docker image ls -aq) || true + swapoff /swapfile || true + rm -rf /swapfile /usr/share/dotnet /usr/local/lib/android /opt/ghc || true + apt purge aria2 ansible hhvm mono-devel azure-cli shellcheck rpm xorriso zsync \ + clang-6.0 lldb-6.0 lld-6.0 clang-format-6.0 clang-8 lldb-8 lld-8 clang-format-8 \ + clang-9 lldb-9 lld-9 clangd-9 clang-format-9 dotnet-sdk-3.0 dotnet-sdk-3.1=3.1.101-1 \ + esl-erlang firefox g++-8 g++-9 gfortran-8 gfortran-9 google-chrome-stable \ + google-cloud-sdk ghc-8.0.2 ghc-8.2.2 ghc-8.4.4 ghc-8.6.2 ghc-8.6.3 ghc-8.6.4 \ + ghc-8.6.5 ghc-8.8.1 ghc-8.8.2 ghc-8.8.3 ghc-8.10.1 cabal-install-2.0 cabal-install-2.2 \ + cabal-install-2.4 cabal-install-3.0 cabal-install-3.2 heroku imagemagick \ + libmagickcore-dev libmagickwand-dev libmagic-dev ant ant-optional kubectl \ + mercurial apt-transport-https mono-complete mysql-client libmysqlclient-dev \ + mysql-server mssql-tools unixodbc-dev yarn bazel chrpath libssl-dev libxft-dev \ + libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev php7.1 php7.1-bcmath \ + php7.1-bz2 php7.1-cgi php7.1-cli php7.1-common php7.1-curl php7.1-dba php7.1-dev \ + php7.1-enchant php7.1-fpm php7.1-gd php7.1-gmp php7.1-imap php7.1-interbase php7.1-intl \ + php7.1-json php7.1-ldap php7.1-mbstring php7.1-mcrypt php7.1-mysql php7.1-odbc \ + php7.1-opcache php7.1-pgsql php7.1-phpdbg php7.1-pspell php7.1-readline php7.1-recode \ + php7.1-snmp php7.1-soap php7.1-sqlite3 php7.1-sybase php7.1-tidy php7.1-xml \ + php7.1-xmlrpc php7.1-xsl php7.1-zip php7.2 php7.2-bcmath php7.2-bz2 php7.2-cgi \ + php7.2-cli php7.2-common php7.2-curl php7.2-dba php7.2-dev php7.2-enchant php7.2-fpm \ + php7.2-gd php7.2-gmp php7.2-imap php7.2-interbase php7.2-intl php7.2-json php7.2-ldap \ + php7.2-mbstring php7.2-mysql php7.2-odbc php7.2-opcache php7.2-pgsql php7.2-phpdbg \ + php7.2-pspell php7.2-readline php7.2-recode php7.2-snmp php7.2-soap php7.2-sqlite3 \ + php7.2-sybase php7.2-tidy php7.2-xml php7.2-xmlrpc php7.2-xsl php7.2-zip php7.3 \ + php7.3-bcmath php7.3-bz2 php7.3-cgi php7.3-cli php7.3-common php7.3-curl php7.3-dba \ + php7.3-dev php7.3-enchant php7.3-fpm php7.3-gd php7.3-gmp php7.3-imap php7.3-interbase \ + php7.3-intl php7.3-json php7.3-ldap php7.3-mbstring php7.3-mysql php7.3-odbc \ + php7.3-opcache php7.3-pgsql php7.3-phpdbg php7.3-pspell php7.3-readline php7.3-recode \ + php7.3-snmp php7.3-soap php7.3-sqlite3 php7.3-sybase php7.3-tidy php7.3-xml \ + php7.3-xmlrpc php7.3-xsl php7.3-zip php7.4 php7.4-bcmath php7.4-bz2 php7.4-cgi \ + php7.4-cli php7.4-common php7.4-curl php7.4-dba php7.4-dev php7.4-enchant php7.4-fpm \ + php7.4-gd php7.4-gmp php7.4-imap php7.4-interbase php7.4-intl php7.4-json php7.4-ldap \ + php7.4-mbstring php7.4-mysql php7.4-odbc php7.4-opcache php7.4-pgsql php7.4-phpdbg \ + php7.4-pspell php7.4-readline php7.4-snmp php7.4-soap php7.4-sqlite3 php7.4-sybase \ + php7.4-tidy php7.4-xml php7.4-xmlrpc php7.4-xsl php7.4-zip php-amqp php-apcu \ + php-igbinary php-memcache php-memcached php-mongodb php-redis php-xdebug \ + php-zmq snmp pollinate libpq-dev postgresql-client powershell ruby-full \ + sphinxsearch subversion mongodb-org -yq >/dev/null 2>&1 || true + apt-get remove -y 'php.*' || true + apt-get autoremove -y >/dev/null 2>&1 || true + apt-get autoclean -y >/dev/null 2>&1 || true + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version-file: './go.mod' + check-latest: true + + # will use the latest release available for ko + - uses: ko-build/setup-ko@3aebd0597dc1e9d1a26bcfdb7cbeb19c131d3037 # v0.7 + + - uses: imranismail/setup-kustomize@2ba527d4d055ab63514ba50a99456fc35684947f # v2.1.0 + + - name: Install yq + uses: mikefarah/yq@bbdd97482f2d439126582a59689eb1c855944955 # v4.44.3 + + - name: Setup mirror + uses: chainguard-dev/actions/setup-mirror@main + with: + mirror: mirror.gcr.io + + - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v2 + + - name: Install cluster + sigstore + uses: sigstore/scaffolding/actions/setup@main + env: + INSTALL_TSA: true + with: + k8s-version: ${{ matrix.k8s-version}} + version: ${{ env.SCAFFOLDING_RELEASE_VERSION }} + + - name: Install policy-controller-with-only-pod-resource + env: + GIT_HASH: ${{ github.sha }} + GIT_VERSION: ci + LDFLAGS: "" + POLICY_CONTROLLER_YAML: test/kustomize-custom-resource/policy-controller-e2e.yaml + KO_PREFIX: registry.local:5000/policy-controller + POLICY_CONTROLLER_ARCHS: linux/amd64 + run: | + make ko-policy-controller + kustomize build test/kustomize-custom-resource | kubectl apply -f - + + # Wait for the webhook to come up and become Ready + kubectl rollout status --timeout 5m --namespace cosign-system deployments/webhook + echo "TUF_ROOT_FILE=./root.json" >> $GITHUB_ENV + + - name: Run Custom resources Tests + timeout-minutes: 5 + run: | + ./test/e2e_test_policy_custom_resource.sh + + - name: Collect diagnostics + if: ${{ failure() }} + uses: chainguard-dev/actions/kind-diag@29fb6e979a0b3efc79748a17e8cec08d0594cbfd # main diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 52f329caa..6e583e2ab 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -21,6 +21,7 @@ import ( "fmt" "log" "os" + "strings" "time" policyduckv1beta1 "github.com/sigstore/policy-controller/pkg/apis/duck/v1beta1" @@ -78,6 +79,13 @@ var ( // https://github.com/sigstore/policy-controller/issues/354 disableTUF = flag.Bool("disable-tuf", false, "Disable TUF support.") + // Validate specific resources. + // https://github.com/sigstore/policy-controller/issues/1388 + resourcesNames = flag.String("resource-name", "replicasets, deployments, pods, cronjobs, jobs, statefulsets, daemonsets", "Comma-separated list of resources") + // Split the input string into a slice of strings + listResources []string + types map[schema.GroupVersionKind]resourcesemantics.GenericCRD + // mutatingCIPWebhookName holds the name of the mutating webhook configuration // resource dispatching admission requests to policy-webhook. // It is also the name of the webhook which is injected by the controller @@ -116,6 +124,8 @@ func main() { flag.IntVar(&opts.Port, "secure-port", opts.Port, "The port on which to serve HTTPS.") flag.Parse() + resourcesNamesList := strings.Split(*resourcesNames, ",") + listResources = append(listResources, resourcesNamesList...) // If TUF has been disabled do not try to set it up. if !*disableTUF { @@ -140,8 +150,7 @@ func main() { // This must match the set of resources we configure in // cmd/webhook/main.go in the "types" map. - common.ValidResourceNames = sets.NewString("replicasets", "deployments", - "pods", "cronjobs", "jobs", "statefulsets", "daemonsets") + common.ValidResourceNames = sets.NewString(resourcesNamesList...) v := version.GetVersionInfo() vJSON, _ := v.JSONString() @@ -198,17 +207,29 @@ func (c *crdEphemeralContainers) SupportedVerbs() []admissionregistrationv1.Oper } } -var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ - corev1.SchemeGroupVersion.WithKind("Pod"): &crdEphemeralContainers{GenericCRD: &duckv1.Pod{}}, - - appsv1.SchemeGroupVersion.WithKind("ReplicaSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("Deployment"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("StatefulSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}}, - appsv1.SchemeGroupVersion.WithKind("DaemonSet"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}}, - batchv1.SchemeGroupVersion.WithKind("Job"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}}, - - batchv1.SchemeGroupVersion.WithKind("CronJob"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}}, - batchv1beta1.SchemeGroupVersion.WithKind("CronJob"): &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}}, +func createTypesMap(kindsList []string) map[schema.GroupVersionKind]resourcesemantics.GenericCRD { + types := make(map[schema.GroupVersionKind]resourcesemantics.GenericCRD) + for _, kind := range kindsList { + kind = strings.TrimSpace(kind) + switch kind { + case "pods": + types[corev1.SchemeGroupVersion.WithKind("Pod")] = &crdEphemeralContainers{GenericCRD: &duckv1.Pod{}} + case "replicasets": + types[appsv1.SchemeGroupVersion.WithKind("ReplicaSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + case "deployments": + types[appsv1.SchemeGroupVersion.WithKind("Deployment")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + case "statefulsets": + types[appsv1.SchemeGroupVersion.WithKind("StatefulSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &policyduckv1beta1.PodScalable{}} + case "daemonsets": + types[appsv1.SchemeGroupVersion.WithKind("DaemonSet")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}} + case "jobs": + types[batchv1.SchemeGroupVersion.WithKind("Job")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.WithPod{}} + case "cronjobs": + types[batchv1.SchemeGroupVersion.WithKind("CronJob")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}} + types[batchv1beta1.SchemeGroupVersion.WithKind("CronJob")] = &crdNoStatusUpdatesOrDeletes{GenericCRD: &duckv1.CronJob{}} + } + } + return types } var typesCIP = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ @@ -221,6 +242,7 @@ var typesCIP = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { // Decorate contexts with the current state of the config. + types = createTypesMap(listResources) store := config.NewStore(logging.FromContext(ctx).Named("config-store")) store.WatchConfigs(cmw) policyControllerConfigStore := policycontrollerconfig.NewStore(logging.FromContext(ctx).Named("config-policy-controller")) @@ -278,7 +300,7 @@ func NewMutatingAdmissionController(ctx context.Context, _ configmap.Watcher) *c } ctx = webhook.WithOptions(ctx, *woptions) validator := cwebhook.NewValidator(ctx) - + types = createTypesMap(listResources) return defaulting.NewAdmissionController(ctx, // Name of the resource webhook. *webhookName, diff --git a/test/e2e_test_cluster_with_scalable.sh b/test/e2e_test_cluster_with_scalable.sh index 1094a4f22..7eedd930c 100755 --- a/test/e2e_test_cluster_with_scalable.sh +++ b/test/e2e_test_cluster_with_scalable.sh @@ -111,6 +111,10 @@ echo '::group:: Deploy deployment with unsigned image' sed "s#TEST_IMAGE#${demoimage}#" ./test/testdata/policy-controller/e2e/test-deployment.yaml | kubectl apply -f - echo '::endgroup::' +echo '::group:: Deploy deployment with custom resource' +sed "s#TEST_IMAGE#${demoimage}#" ./test/testdata/policy-controller/e2e/test-deployment-with-custom-resource.yaml | kubectl apply -f - +echo '::endgroup::' + echo '::group:: Label test namespace for verification' kubectl label namespace ${NS} policy.sigstore.dev/include=true echo '::endgroup::' diff --git a/test/e2e_test_policy_custom_resource.sh b/test/e2e_test_policy_custom_resource.sh new file mode 100644 index 000000000..f2d608210 --- /dev/null +++ b/test/e2e_test_policy_custom_resource.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# Copyright 2024 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. + +set -ex + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +# Variables +export CUSTOM_RESOURCE="DaemonSet,ReplicaSets" +export NS=custom-resource-test +export TIMESTAMP="TIMESTAMP" + +# Helper function to validate webhook configuration +assert_webhook_configuration() { + local webhook_name=$1 + local resource=$2 + + echo "Validating ${webhook_name} for resource ${resource}" + kubectl get ${webhook_name} -o yaml | grep -q "resources:.*${resource}" || { + echo "Resource ${resource} not found in ${webhook_name}" + exit 1 + } + echo "Resource ${resource} found in ${webhook_name}" +} + +# Helper function to check if an image uses a tag +assert_image_tag() { + local pod_name=$1 + local container_name=$2 + + echo "Checking that ${pod_name}/${container_name} uses a tag, not a digest" + image=$(kubectl get pod "${pod_name}" -n "${NS}" -o=jsonpath="{.spec.containers[?(@.name=='${container_name}')].image}") + if [[ "${image}" =~ @sha256:[a-f0-9]{64} ]]; then + echo "Image ${image} is using a digest, which is not allowed" + exit 1 + else + echo "Image ${image} is correctly using a tag" + fi +} + +# Step 1: Create namespace +echo '::group:: Create and label namespace for testing' +kubectl create namespace ${NS} +kubectl label namespace ${NS} policy.sigstore.dev/include=true +echo '::endgroup::' + +# Step 2: Deploy Policy Controller with custom-resource flag +echo '::group:: Deploy Policy Controller with custom-resource flag' +KO_DOCKER_REPO=${KO_DOCKER_REPO} ko apply -f ./deploy/manifests.yaml \ + --set-string policyController.customResource=${CUSTOM_RESOURCE} +echo '::endgroup::' + +# Step 3: Validate webhook configurations +echo '::group:: Validate webhook configurations' +sleep 5 # Allow webhook configurations to propagate + +for resource in DaemonSet ReplicaSets; do + assert_webhook_configuration "MutatingWebhookConfiguration" "${resource}" + assert_webhook_configuration "ValidatingWebhookConfiguration" "${resource}" +done + +# Ensure a non-monitored resource is NOT included +if kubectl get MutatingWebhookConfiguration -o yaml | grep -q "resources:.*Pods"; then + echo "Pods should not be included in MutatingWebhookConfiguration" + exit 1 +else + echo "Pods correctly excluded from MutatingWebhookConfiguration" +fi +echo '::endgroup::' + +# Step 4: Deploy sandbox and check for image tag usage +echo '::group:: Deploy sandbox and check image tags' +cat <