Refactor fuzzing
Structure the fuzz implementation to be closer to what go native will support. Add Makefile target to enable smoketesting fuzzers. Add smoketest as CI workflow. Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
This commit is contained in:
parent
7f8441672e
commit
50c043eb4f
|
|
@ -0,0 +1,20 @@
|
||||||
|
name: CIFuzz
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
Fuzzing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Restore Go cache
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: /home/runner/work/_temp/_github_home/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
- name: Smoke test Fuzzers
|
||||||
|
run: make fuzz-smoketest
|
||||||
|
|
@ -13,8 +13,9 @@
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
testbin/
|
|
||||||
bin/
|
bin/
|
||||||
config/release/
|
config/release/
|
||||||
config/crd/bases/gitrepositories.yaml
|
config/crd/bases/gitrepositories.yaml
|
||||||
config/crd/bases/buckets.yaml
|
config/crd/bases/buckets.yaml
|
||||||
|
|
||||||
|
build/
|
||||||
|
|
|
||||||
21
Makefile
21
Makefile
|
|
@ -22,7 +22,7 @@ ENVTEST_ARCH ?= amd64
|
||||||
all: manager
|
all: manager
|
||||||
|
|
||||||
# Download the envtest binaries to testbin
|
# Download the envtest binaries to testbin
|
||||||
ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
|
ENVTEST_ASSETS_DIR=$(shell pwd)/build/testbin
|
||||||
ENVTEST_KUBERNETES_VERSION?=latest
|
ENVTEST_KUBERNETES_VERSION?=latest
|
||||||
install-envtest: setup-envtest
|
install-envtest: setup-envtest
|
||||||
mkdir -p ${ENVTEST_ASSETS_DIR}
|
mkdir -p ${ENVTEST_ASSETS_DIR}
|
||||||
|
|
@ -147,3 +147,22 @@ GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
|
||||||
rm -rf $$TMP_DIR ;\
|
rm -rf $$TMP_DIR ;\
|
||||||
}
|
}
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
# Build fuzzers
|
||||||
|
fuzz-build:
|
||||||
|
rm -rf $(shell pwd)/build/fuzz/
|
||||||
|
mkdir -p $(shell pwd)/build/fuzz/out/
|
||||||
|
|
||||||
|
docker build . --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
|
||||||
|
docker run --rm \
|
||||||
|
-e FUZZING_LANGUAGE=go -e SANITIZER=address \
|
||||||
|
-e CIFUZZ_DEBUG='True' -e OSS_FUZZ_PROJECT_NAME=fluxcd \
|
||||||
|
-v "$(shell pwd)/build/fuzz/out":/out \
|
||||||
|
local-fuzzing:latest
|
||||||
|
|
||||||
|
fuzz-smoketest: fuzz-build
|
||||||
|
docker run --rm \
|
||||||
|
-v "$(shell pwd)/build/fuzz/out":/out \
|
||||||
|
-v "$(shell pwd)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \
|
||||||
|
local-fuzzing:latest \
|
||||||
|
bash -c "/runner.sh"
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ var (
|
||||||
debugMode = os.Getenv("DEBUG_TEST") != ""
|
debugMode = os.Getenv("DEBUG_TEST") != ""
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func runInContext(registerControllers func(*testenv.Environment), run func() error, crdPath string) error {
|
||||||
var err error
|
var err error
|
||||||
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
|
utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme))
|
||||||
utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme))
|
utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme))
|
||||||
|
|
@ -78,9 +78,7 @@ func TestMain(m *testing.M) {
|
||||||
controllerLog.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(false)))
|
controllerLog.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(false)))
|
||||||
}
|
}
|
||||||
|
|
||||||
testEnv = testenv.New(testenv.WithCRDPath(
|
testEnv = testenv.New(testenv.WithCRDPath(crdPath))
|
||||||
filepath.Join("..", "config", "crd", "bases"),
|
|
||||||
))
|
|
||||||
|
|
||||||
testServer, err = testserver.NewTempArtifactServer()
|
testServer, err = testserver.NewTempArtifactServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -89,18 +87,7 @@ func TestMain(m *testing.M) {
|
||||||
fmt.Println("Starting the test storage server")
|
fmt.Println("Starting the test storage server")
|
||||||
testServer.Start()
|
testServer.Start()
|
||||||
|
|
||||||
controllerName := "kustomize-controller"
|
registerControllers(testEnv)
|
||||||
testEventsH = controller.MakeEvents(testEnv, controllerName, nil)
|
|
||||||
testMetricsH = controller.MustMakeMetrics(testEnv)
|
|
||||||
reconciler := &KustomizationReconciler{
|
|
||||||
ControllerName: controllerName,
|
|
||||||
Client: testEnv,
|
|
||||||
EventRecorder: testEventsH.EventRecorder,
|
|
||||||
MetricsRecorder: testMetricsH.MetricsRecorder,
|
|
||||||
}
|
|
||||||
if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{MaxConcurrentReconciles: 4}); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Println("Starting the test environment")
|
fmt.Println("Starting the test environment")
|
||||||
|
|
@ -129,7 +116,7 @@ func TestMain(m *testing.M) {
|
||||||
panic(fmt.Sprintf("Failed to create k8s client: %v", err))
|
panic(fmt.Sprintf("Failed to create k8s client: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
code := m.Run()
|
runErr := run()
|
||||||
|
|
||||||
if debugMode {
|
if debugMode {
|
||||||
events := &corev1.EventList{}
|
events := &corev1.EventList{}
|
||||||
|
|
@ -152,6 +139,30 @@ func TestMain(m *testing.M) {
|
||||||
panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
|
panic(fmt.Sprintf("Failed to remove storage server dir: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
code := 0
|
||||||
|
|
||||||
|
runInContext(func(testEnv *testenv.Environment) {
|
||||||
|
controllerName := "kustomize-controller"
|
||||||
|
testEventsH = controller.MakeEvents(testEnv, controllerName, nil)
|
||||||
|
testMetricsH = controller.MustMakeMetrics(testEnv)
|
||||||
|
reconciler := &KustomizationReconciler{
|
||||||
|
ControllerName: controllerName,
|
||||||
|
Client: testEnv,
|
||||||
|
EventRecorder: testEventsH.EventRecorder,
|
||||||
|
MetricsRecorder: testMetricsH.MetricsRecorder,
|
||||||
|
}
|
||||||
|
if err := (reconciler).SetupWithManager(testEnv, KustomizationReconcilerOptions{MaxConcurrentReconciles: 4}); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err))
|
||||||
|
}
|
||||||
|
}, func() error {
|
||||||
|
code = m.Run()
|
||||||
|
return nil
|
||||||
|
}, filepath.Join("..", "config", "crd", "bases"))
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
FROM golang:1.16-buster as builder
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y git vim clang
|
|
||||||
RUN git clone https://github.com/fluxcd/kustomize-controller /kustomize-controller
|
|
||||||
|
|
||||||
WORKDIR /kustomize-controller
|
|
||||||
|
|
||||||
# fillippo.io/age v1.0.0-beta7 throws an error
|
|
||||||
RUN sed 's/filippo.io\/age v1.0.0-beta7/filippo.io\/age v1.0.0/g' -i /kustomize-controller/go.mod
|
|
||||||
RUN make download-crd-deps
|
|
||||||
RUN mkdir /kustomize-controller/fuzz
|
|
||||||
COPY fuzz.go /kustomize-controller/controllers/
|
|
||||||
|
|
||||||
RUN go mod tidy
|
|
||||||
|
|
||||||
RUN cd / \
|
|
||||||
&& go get -u github.com/dvyukov/go-fuzz/go-fuzz@latest github.com/dvyukov/go-fuzz/go-fuzz-build@latest
|
|
||||||
RUN go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
|
||||||
RUN go get github.com/AdaLogics/go-fuzz-headers
|
|
||||||
RUN go mod download golang.org/x/sync
|
|
||||||
RUN go mod download github.com/dvyukov/go-fuzz
|
|
||||||
|
|
||||||
RUN mkdir /fuzzers
|
|
||||||
RUN cd /kustomize-controller/controllers \
|
|
||||||
&& go-fuzz-build -libfuzzer -func=Fuzz \
|
|
||||||
&& clang -o /fuzzers/Fuzz reflect-fuzz.a \
|
|
||||||
-fsanitize=fuzzer
|
|
||||||
|
|
||||||
RUN cd /kustomize-controller/controllers && /fuzzers/Fuzz
|
|
||||||
591
fuzz/fuzz.go
591
fuzz/fuzz.go
|
|
@ -1,591 +0,0 @@
|
||||||
// +build gofuzz
|
|
||||||
/*
|
|
||||||
Copyright 2021 The Flux 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 controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
|
|
||||||
"github.com/fluxcd/pkg/runtime/controller"
|
|
||||||
"github.com/fluxcd/pkg/testserver"
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
|
||||||
|
|
||||||
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
var cfg *rest.Config
|
|
||||||
var k8sClient client.Client
|
|
||||||
var k8sManager ctrl.Manager
|
|
||||||
var testEnv *envtest.Environment
|
|
||||||
var kubeConfig []byte
|
|
||||||
var testServer *testserver.ArtifactServer
|
|
||||||
var testEventsH controller.Events
|
|
||||||
var testMetricsH controller.Metrics
|
|
||||||
var ctx = ctrl.SetupSignalHandler()
|
|
||||||
var initter sync.Once
|
|
||||||
var runningInOssfuzz = false
|
|
||||||
var (
|
|
||||||
localCRDpath = []string{filepath.Join("..", "config", "crd", "bases")}
|
|
||||||
|
|
||||||
// Variables for the OSS-fuzz environment:
|
|
||||||
downloadLink = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-1.19.2-linux-amd64.tar.gz"
|
|
||||||
downloadPath = "/tmp/envtest-bins.tar.gz"
|
|
||||||
binariesDir = "/tmp/test-binaries"
|
|
||||||
ossFuzzCrdPath = []string{filepath.Join(".", "bases")}
|
|
||||||
ossFuzzCrdYaml = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml"
|
|
||||||
ossFuzzGitYaml = "https://raw.githubusercontent.com/fluxcd/source-controller/v0.15.4/config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml"
|
|
||||||
ossFuzzBucYaml = "https://raw.githubusercontent.com/fluxcd/source-controller/v0.15.4/config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml"
|
|
||||||
crdPath []string
|
|
||||||
pgpAscFile = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/controllers/testdata/sops/pgp.asc"
|
|
||||||
ageTxtFile = "https://raw.githubusercontent.com/fluxcd/kustomize-controller/main/controllers/testdata/sops/age.txt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// createKUBEBUILDER_ASSETS runs "setup-envtest use" and
|
|
||||||
// returns the path of the 3 binaries that are created.
|
|
||||||
// If this one fails, it means that setup-envtest is not
|
|
||||||
// available in the runtime environment, and that means
|
|
||||||
// that the fuzzer is being run by OSS-fuzz. In that case
|
|
||||||
// we set "runningInOssfuzz" to true which will later trigger
|
|
||||||
// download of all required files so the OSS-fuzz can run
|
|
||||||
// the fuzzer.
|
|
||||||
func createKUBEBUILDER_ASSETS() string {
|
|
||||||
out, err := exec.Command("setup-envtest", "use").Output()
|
|
||||||
if err != nil {
|
|
||||||
// If there is an error here, the fuzzer is running
|
|
||||||
// in OSS-fuzz where the binary setup-envtest is
|
|
||||||
// not available, so we have to get them in an
|
|
||||||
// alternative way
|
|
||||||
runningInOssfuzz = true
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// split the output:
|
|
||||||
splitString := strings.Split(string(out), " ")
|
|
||||||
binPath := strings.TrimSuffix(splitString[len(splitString)-1], "\n")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return binPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downloads a file to a path. This is only needed when
|
|
||||||
// the fuzzer is running in OSS-fuzz.
|
|
||||||
func DownloadFile(filepath string, url string) error {
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Create the file
|
|
||||||
out, err := os.Create(filepath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
// Write the body to file
|
|
||||||
_, err = io.Copy(out, resp.Body)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// When OSS-fuzz runs the fuzzer a few files need to
|
|
||||||
// be download during initialization. No files from the
|
|
||||||
// kustomize-controller repo are available at runtime,
|
|
||||||
// and each of the files that are needed must be downloaded
|
|
||||||
// manually.
|
|
||||||
func downloadFilesForOssFuzz() error {
|
|
||||||
err := DownloadFile(downloadPath, downloadLink)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.MkdirAll(binariesDir, 0777)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cmd := exec.Command("tar", "xvf", downloadPath, "-C", binariesDir)
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download crds
|
|
||||||
err = os.MkdirAll("bases", 0777)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = DownloadFile("./bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml", ossFuzzCrdYaml)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = DownloadFile("./bases/source.toolkit.fluxcd.io_gitrepositories.yaml", ossFuzzGitYaml)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = DownloadFile("./bases/source.toolkit.fluxcd.io_buckets.yaml", ossFuzzBucYaml)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download sops files
|
|
||||||
err = os.MkdirAll("testdata/sops", 0777)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = DownloadFile("./testdata/sops/pgp.asc", pgpAscFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = DownloadFile("./testdata/sops/age.txt", ageTxtFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// customInit implements an init function that
|
|
||||||
// is invoked by way of sync.Do
|
|
||||||
func customInit() {
|
|
||||||
kubebuilder_assets := createKUBEBUILDER_ASSETS()
|
|
||||||
os.Setenv("KUBEBUILDER_ASSETS", kubebuilder_assets)
|
|
||||||
|
|
||||||
// Set up things for fuzzing in the OSS-fuzz environment:
|
|
||||||
if runningInOssfuzz {
|
|
||||||
err := downloadFilesForOssFuzz()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
os.Setenv("KUBEBUILDER_ASSETS", binariesDir+"/kubebuilder/bin")
|
|
||||||
crdPath = ossFuzzCrdPath
|
|
||||||
runningInOssfuzz = false
|
|
||||||
} else {
|
|
||||||
crdPath = localCRDpath
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
err = sourcev1.AddToScheme(scheme.Scheme)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
err = kustomizev1.AddToScheme(scheme.Scheme)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testEnv = &envtest.Environment{
|
|
||||||
CRDDirectoryPaths: crdPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
testServer, err = testserver.NewTempArtifactServer()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create a temporary storage server: %v", err))
|
|
||||||
}
|
|
||||||
fmt.Println("Starting the test storage server")
|
|
||||||
testServer.Start()
|
|
||||||
|
|
||||||
cfg, err = testEnv.Start()
|
|
||||||
|
|
||||||
user, err := testEnv.ControlPlane.AddUser(envtest.User{
|
|
||||||
Name: "envtest-admin",
|
|
||||||
Groups: []string{"system:masters"},
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create envtest-admin user: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
kubeConfig, err = user.KubeConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create the envtest-admin user kubeconfig: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// client with caching disabled
|
|
||||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
|
||||||
|
|
||||||
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
|
|
||||||
Scheme: scheme.Scheme,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := (&KustomizationReconciler{
|
|
||||||
Client: k8sManager.GetClient(),
|
|
||||||
}).SetupWithManager(k8sManager, KustomizationReconcilerOptions{MaxConcurrentReconciles: 1}); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
|
|
||||||
}
|
|
||||||
time.Sleep(1*time.Second)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
fmt.Println("Starting the test environment")
|
|
||||||
if err := k8sManager.Start(ctx); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to start the test environment manager: %v", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
time.Sleep(time.Second*1)
|
|
||||||
<-k8sManager.Elected()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fuzz implements the fuzz harness
|
|
||||||
func Fuzz(data []byte) int {
|
|
||||||
initter.Do(customInit)
|
|
||||||
f := fuzz.NewConsumer(data)
|
|
||||||
dname, err := os.MkdirTemp("", "artifact-dir")
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dname)
|
|
||||||
err = f.CreateFiles(dname)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
id, err := randString(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
namespace, err := createNamespaceForFuzzing(id)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer k8sClient.Delete(context.Background(), namespace)
|
|
||||||
|
|
||||||
fmt.Println("createKubeConfigSecretForFuzzing...")
|
|
||||||
secret, err := createKubeConfigSecretForFuzzing(id)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer k8sClient.Delete(context.Background(), secret)
|
|
||||||
|
|
||||||
artifactFile, err := randString(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
fmt.Println("createArtifact...")
|
|
||||||
artifactChecksum, err := createArtifact(testServer, dname, artifactFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
_ = artifactChecksum
|
|
||||||
fmt.Println("testServer.URLForFile...")
|
|
||||||
artifactURL, err := testServer.URLForFile(artifactFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("URLForFile error: ", err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
_ = artifactURL
|
|
||||||
repName, err := randString(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
repositoryName := types.NamespacedName{
|
|
||||||
Name: repName,
|
|
||||||
Namespace: id,
|
|
||||||
}
|
|
||||||
fmt.Println("applyGitRepository...")
|
|
||||||
err = applyGitRepository(repositoryName, artifactURL, "main/"+artifactChecksum, artifactChecksum)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
pgpKey, err := ioutil.ReadFile("testdata/sops/pgp.asc")
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
ageKey, err := ioutil.ReadFile("testdata/sops/age.txt")
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
sskName, err := randString(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
sopsSecretKey := types.NamespacedName{
|
|
||||||
Name: sskName,
|
|
||||||
Namespace: id,
|
|
||||||
}
|
|
||||||
|
|
||||||
sopsSecret := &corev1.Secret{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: sopsSecretKey.Name,
|
|
||||||
Namespace: sopsSecretKey.Namespace,
|
|
||||||
},
|
|
||||||
StringData: map[string]string{
|
|
||||||
"pgp.asc": string(pgpKey),
|
|
||||||
"age.agekey": string(ageKey),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = k8sClient.Create(context.Background(), sopsSecret)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer k8sClient.Delete(context.Background(), sopsSecret)
|
|
||||||
|
|
||||||
kkName, err := randString(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
kustomizationKey := types.NamespacedName{
|
|
||||||
Name: kkName,
|
|
||||||
Namespace: id,
|
|
||||||
}
|
|
||||||
kustomization := &kustomizev1.Kustomization{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: kustomizationKey.Name,
|
|
||||||
Namespace: kustomizationKey.Namespace,
|
|
||||||
},
|
|
||||||
Spec: kustomizev1.KustomizationSpec{
|
|
||||||
Path: "./",
|
|
||||||
KubeConfig: &kustomizev1.KubeConfig{
|
|
||||||
SecretRef: meta.LocalObjectReference{
|
|
||||||
Name: "kubeconfig",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SourceRef: kustomizev1.CrossNamespaceSourceReference{
|
|
||||||
Name: repositoryName.Name,
|
|
||||||
Namespace: repositoryName.Namespace,
|
|
||||||
Kind: sourcev1.GitRepositoryKind,
|
|
||||||
},
|
|
||||||
Decryption: &kustomizev1.Decryption{
|
|
||||||
Provider: "sops",
|
|
||||||
SecretRef: &meta.LocalObjectReference{
|
|
||||||
Name: sopsSecretKey.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
TargetNamespace: id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = k8sClient.Create(context.TODO(), kustomization)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
defer k8sClient.Delete(context.TODO(), kustomization)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows the fuzzer to create a random lowercase string
|
|
||||||
func randString(f *fuzz.ConsumeFuzzer) (string, error) {
|
|
||||||
stringLength, err := f.GetInt()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
maxLength := 50
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
for i:=0;i<stringLength%maxLength;i++ {
|
|
||||||
getByte, err := f.GetByte()
|
|
||||||
if err != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
if getByte < 'a' || getByte > 'z' {
|
|
||||||
return "", errors.New("Not a good char")
|
|
||||||
}
|
|
||||||
buffer.WriteByte(getByte)
|
|
||||||
}
|
|
||||||
return buffer.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a namespace.
|
|
||||||
// Caller must delete the created namespace.
|
|
||||||
func createNamespaceForFuzzing(name string) (*corev1.Namespace, error) {
|
|
||||||
namespace := &corev1.Namespace{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: name},
|
|
||||||
}
|
|
||||||
err := k8sClient.Create(context.Background(), namespace)
|
|
||||||
if err != nil {
|
|
||||||
return namespace, err
|
|
||||||
}
|
|
||||||
return namespace, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a secret.
|
|
||||||
// Caller must delete the created secret.
|
|
||||||
func createKubeConfigSecretForFuzzing(namespace string) (*corev1.Secret, error) {
|
|
||||||
secret := &corev1.Secret{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "kubeconfig",
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Data: map[string][]byte{
|
|
||||||
"value.yaml": kubeConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := k8sClient.Create(context.Background(), secret)
|
|
||||||
if err != nil {
|
|
||||||
return secret, err
|
|
||||||
}
|
|
||||||
return secret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// taken from https://github.com/fluxcd/kustomize-controller/blob/main/controllers/suite_test.go#L222
|
|
||||||
func createArtifact(artifactServer *testserver.ArtifactServer, fixture, path string) (string, error) {
|
|
||||||
if f, err := os.Stat(fixture); os.IsNotExist(err) || !f.IsDir() {
|
|
||||||
return "", fmt.Errorf("invalid fixture path: %s", fixture)
|
|
||||||
}
|
|
||||||
f, err := os.Create(filepath.Join(artifactServer.Root(), path))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(f.Name())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
h := sha1.New()
|
|
||||||
|
|
||||||
mw := io.MultiWriter(h, f)
|
|
||||||
gw := gzip.NewWriter(mw)
|
|
||||||
tw := tar.NewWriter(gw)
|
|
||||||
|
|
||||||
if err = filepath.Walk(fixture, func(p string, fi os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore anything that is not a file (directories, symlinks)
|
|
||||||
if !fi.Mode().IsRegular() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore dotfiles
|
|
||||||
if strings.HasPrefix(fi.Name(), ".") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
header, err := tar.FileInfoHeader(fi, p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
relFilePath := p
|
|
||||||
if filepath.IsAbs(fixture) {
|
|
||||||
relFilePath, err = filepath.Rel(fixture, p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header.Name = relFilePath
|
|
||||||
|
|
||||||
if err := tw.WriteHeader(header); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
f.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(tw, f); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Close()
|
|
||||||
}); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tw.Close(); err != nil {
|
|
||||||
gw.Close()
|
|
||||||
f.Close()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := gw.Close(); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(f.Name(), 0644); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from https://github.com/fluxcd/kustomize-controller/blob/main/controllers/suite_test.go#L171
|
|
||||||
func applyGitRepository(objKey client.ObjectKey, artifactURL, artifactRevision, artifactChecksum string) error {
|
|
||||||
repo := &sourcev1.GitRepository{
|
|
||||||
TypeMeta: metav1.TypeMeta{
|
|
||||||
Kind: sourcev1.GitRepositoryKind,
|
|
||||||
APIVersion: sourcev1.GroupVersion.String(),
|
|
||||||
},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: objKey.Name,
|
|
||||||
Namespace: objKey.Namespace,
|
|
||||||
},
|
|
||||||
Spec: sourcev1.GitRepositorySpec{
|
|
||||||
URL: "https://github.com/test/repository",
|
|
||||||
Interval: metav1.Duration{Duration: time.Minute},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
status := sourcev1.GitRepositoryStatus{
|
|
||||||
Conditions: []metav1.Condition{
|
|
||||||
{
|
|
||||||
Type: meta.ReadyCondition,
|
|
||||||
Status: metav1.ConditionTrue,
|
|
||||||
LastTransitionTime: metav1.Now(),
|
|
||||||
Reason: sourcev1.GitOperationSucceedReason,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Artifact: &sourcev1.Artifact{
|
|
||||||
Path: artifactURL,
|
|
||||||
URL: artifactURL,
|
|
||||||
Revision: artifactRevision,
|
|
||||||
Checksum: artifactChecksum,
|
|
||||||
LastUpdateTime: metav1.Now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := []client.PatchOption{
|
|
||||||
client.ForceOwnership,
|
|
||||||
client.FieldOwner("kustomize-controller"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := k8sClient.Patch(context.Background(), repo, client.Apply, opt...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.ManagedFields = nil
|
|
||||||
repo.Status = status
|
|
||||||
if err := k8sClient.Status().Patch(context.Background(), repo, client.Apply, opt...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM gcr.io/oss-fuzz-base/base-builder-go
|
||||||
|
|
||||||
|
COPY ./ $GOPATH/src/github.com/fluxcd/kustomize-controller/
|
||||||
|
COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh
|
||||||
|
|
||||||
|
WORKDIR $SRC
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# fuzz testing
|
||||||
|
|
||||||
|
Flux is part of Google's [oss fuzz] program which provides continuous fuzzing for
|
||||||
|
open source projects.
|
||||||
|
|
||||||
|
The long running fuzzing execution is configured in the [oss-fuzz repository].
|
||||||
|
Shorter executions are done on a per-PR basis, configured as a [github workflow].
|
||||||
|
|
||||||
|
For fuzzers to be called, they must be compiled within [oss_fuzz_build.sh](./oss_fuzz_build.sh).
|
||||||
|
|
||||||
|
### Testing locally
|
||||||
|
|
||||||
|
Build fuzzers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fuzz-build
|
||||||
|
```
|
||||||
|
All fuzzers will be built into `./build/fuzz/out`.
|
||||||
|
|
||||||
|
Smoke test fuzzers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fuzz-smoketest
|
||||||
|
```
|
||||||
|
|
||||||
|
The smoke test runs each fuzzer once to ensure they are fully functional.
|
||||||
|
|
||||||
|
Run fuzzer locally:
|
||||||
|
```bash
|
||||||
|
./build/fuzz/out/fuzz_conditions_match
|
||||||
|
```
|
||||||
|
|
||||||
|
Run fuzzer inside a container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -ti \
|
||||||
|
-v "$(pwd)/build/fuzz/out":/out \
|
||||||
|
gcr.io/oss-fuzz/fluxcd \
|
||||||
|
/out/fuzz_conditions_match
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[oss fuzz]: https://github.com/google/oss-fuzz
|
||||||
|
[oss-fuzz repository]: https://github.com/google/oss-fuzz/tree/master/projects/fluxcd
|
||||||
|
[github workflow]: .github/workflows/cifuzz.yaml
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Flux 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 age
|
||||||
|
|
||||||
|
import (
|
||||||
|
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzAge implements a fuzzer that targets functions within age/keysource.go.
|
||||||
|
func FuzzAge(data []byte) int {
|
||||||
|
f := fuzz.NewConsumer(data)
|
||||||
|
masterKey := MasterKey{}
|
||||||
|
|
||||||
|
if err := f.GenerateStruct(&masterKey); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = masterKey.Encrypt(data)
|
||||||
|
_ = masterKey.EncryptIfNeeded(data)
|
||||||
|
|
||||||
|
receipts, err := f.GetString()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = MasterKeysFromRecipients(receipts)
|
||||||
|
_, _ = MasterKeyFromRecipient(receipts)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,441 @@
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Flux 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 controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||||||
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
|
||||||
|
"github.com/fluxcd/kustomize-controller/controllers"
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/fluxcd/pkg/runtime/testenv"
|
||||||
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
||||||
|
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var doOnce sync.Once
|
||||||
|
|
||||||
|
const defaultBinVersion = "1.23"
|
||||||
|
|
||||||
|
func envtestBinVersion() string {
|
||||||
|
if binVersion := os.Getenv("ENVTEST_BIN_VERSION"); binVersion != "" {
|
||||||
|
return binVersion
|
||||||
|
}
|
||||||
|
return defaultBinVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed testdata/crd/*.yaml
|
||||||
|
//go:embed testdata/sops/pgp.asc
|
||||||
|
//go:embed testdata/sops/age.txt
|
||||||
|
var testFiles embed.FS
|
||||||
|
|
||||||
|
// ensureDependencies ensure that:
|
||||||
|
// a) setup-envtest is installed and a specific version of envtest is deployed.
|
||||||
|
// b) the embedded crd files are exported onto the "runner container".
|
||||||
|
//
|
||||||
|
// The steps above are important as the fuzzers tend to be built in an
|
||||||
|
// environment (or container) and executed in other.
|
||||||
|
func ensureDependencies() error {
|
||||||
|
// only install dependencies when running inside a container
|
||||||
|
if _, err := os.Stat("/.dockerenv"); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
|
||||||
|
binVersion := envtestBinVersion()
|
||||||
|
cmd := exec.Command("/usr/bin/bash", "-c", fmt.Sprintf(`go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest && \
|
||||||
|
/root/go/bin/setup-envtest use -p path %s`, binVersion))
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), "GOPATH=/root/go")
|
||||||
|
assetsPath, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Setenv("KUBEBUILDER_ASSETS", string(assetsPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output all embedded testdata files
|
||||||
|
// "testdata/sops" does not need to be saved in disk
|
||||||
|
// as it is being consumed directly from the embed.FS.
|
||||||
|
embedDirs := []string{"testdata/crd"}
|
||||||
|
for _, dir := range embedDirs {
|
||||||
|
err := os.MkdirAll(dir, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mkdir %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
templates, err := fs.ReadDir(testFiles, dir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading embedded dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, template := range templates {
|
||||||
|
fileName := fmt.Sprintf("%s/%s", dir, template.Name())
|
||||||
|
fmt.Println(fileName)
|
||||||
|
|
||||||
|
data, err := testFiles.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading embedded file %s: %v", fileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.WriteFile(fileName, data, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing %s: %v", fileName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzControllers implements a fuzzer that targets the Kustomize controller.
|
||||||
|
//
|
||||||
|
// The test must ensure a valid test state around Kubernetes objects, as the
|
||||||
|
// focus is to ensure the controller behaves properly, not Kubernetes nor
|
||||||
|
// testing infrastructure.
|
||||||
|
func FuzzControllers(data []byte) int {
|
||||||
|
// Fuzzing has to be deterministic, so that input A can be
|
||||||
|
// associated with crash B consistently. The current approach
|
||||||
|
// taken by go-fuzz-headers to achieve that uses the data input
|
||||||
|
// as a buffer which is used until its end (i.e. EOF).
|
||||||
|
//
|
||||||
|
// The problem with this approach is when the test setup requires
|
||||||
|
// a higher amount of input than the available in the buffer,
|
||||||
|
// resulting in an invalid test state.
|
||||||
|
//
|
||||||
|
// This is currently being countered by openning two consumers on
|
||||||
|
// the data input, and requiring at least a length of 1000 to
|
||||||
|
// run the tests.
|
||||||
|
//
|
||||||
|
// During the migration to the native fuzz feature in go we should
|
||||||
|
// review this approach.
|
||||||
|
if len(data) < 1000 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
fmt.Printf("Data input length: %d\n", len(data))
|
||||||
|
|
||||||
|
f := fuzz.NewConsumer(data)
|
||||||
|
ff := fuzz.NewConsumer(data)
|
||||||
|
|
||||||
|
doOnce.Do(func() {
|
||||||
|
if err := ensureDependencies(); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to ensure dependencies: %v", err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runInContext(func(testEnv *testenv.Environment) {
|
||||||
|
controllerName := "kustomize-controller"
|
||||||
|
reconciler := &controllers.KustomizationReconciler{
|
||||||
|
ControllerName: controllerName,
|
||||||
|
Client: testEnv,
|
||||||
|
}
|
||||||
|
if err := (reconciler).SetupWithManager(testEnv, controllers.KustomizationReconcilerOptions{MaxConcurrentReconciles: 1}); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
|
||||||
|
}
|
||||||
|
}, func() error {
|
||||||
|
dname, err := os.MkdirTemp("", "artifact-dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dname)
|
||||||
|
|
||||||
|
if err = createFiles(ff, dname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namespaceName, err := randStringRange(f, 1, 63)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, err := createNamespaceForFuzzing(namespaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer k8sClient.Delete(context.Background(), namespace)
|
||||||
|
|
||||||
|
fmt.Println("createKubeConfigSecretForFuzzing...")
|
||||||
|
secret, err := createKubeConfigSecretForFuzzing(namespaceName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer k8sClient.Delete(context.Background(), secret)
|
||||||
|
|
||||||
|
artifactFile, err := randStringRange(f, 1, 63)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("createArtifact...")
|
||||||
|
artifactChecksum, err := createArtifact(testServer, dname, artifactFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repName, err := randStringRange(f, 1, 63)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repositoryName := types.NamespacedName{
|
||||||
|
Name: repName,
|
||||||
|
Namespace: namespaceName,
|
||||||
|
}
|
||||||
|
fmt.Println("ApplyGitRepository...")
|
||||||
|
err = applyGitRepository(repositoryName, artifactFile, "main/"+artifactChecksum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pgpKey, err := testFiles.ReadFile("testdata/sops/pgp.asc")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ageKey, err := testFiles.ReadFile("testdata/sops/age.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sskName, err := randStringRange(f, 1, 63)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sopsSecretKey := types.NamespacedName{
|
||||||
|
Name: sskName,
|
||||||
|
Namespace: namespaceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
sopsSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: sopsSecretKey.Name,
|
||||||
|
Namespace: sopsSecretKey.Namespace,
|
||||||
|
},
|
||||||
|
StringData: map[string]string{
|
||||||
|
"pgp.asc": string(pgpKey),
|
||||||
|
"age.agekey": string(ageKey),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = k8sClient.Create(context.Background(), sopsSecret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer k8sClient.Delete(context.Background(), sopsSecret)
|
||||||
|
|
||||||
|
kkName, err := randStringRange(f, 1, 63)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kustomizationKey := types.NamespacedName{
|
||||||
|
Name: kkName,
|
||||||
|
Namespace: namespaceName,
|
||||||
|
}
|
||||||
|
kustomization := &kustomizev1.Kustomization{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: kustomizationKey.Name,
|
||||||
|
Namespace: kustomizationKey.Namespace,
|
||||||
|
},
|
||||||
|
Spec: kustomizev1.KustomizationSpec{
|
||||||
|
Path: "./",
|
||||||
|
KubeConfig: &kustomizev1.KubeConfig{
|
||||||
|
SecretRef: meta.LocalObjectReference{
|
||||||
|
Name: "kubeconfig",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRef: kustomizev1.CrossNamespaceSourceReference{
|
||||||
|
Name: repositoryName.Name,
|
||||||
|
Namespace: repositoryName.Namespace,
|
||||||
|
Kind: sourcev1.GitRepositoryKind,
|
||||||
|
},
|
||||||
|
Decryption: &kustomizev1.Decryption{
|
||||||
|
Provider: "sops",
|
||||||
|
SecretRef: &meta.LocalObjectReference{
|
||||||
|
Name: sopsSecretKey.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TargetNamespace: namespaceName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = k8sClient.Create(context.TODO(), kustomization); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure reconciliation of the kustomization above took place before moving on
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
if err = k8sClient.Delete(context.TODO(), kustomization); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the deferred deletion of all objects (namespace, secret, sopSecret) and
|
||||||
|
// the kustomization above were reconciled before moving on. This avoids unneccessary
|
||||||
|
// errors whilst tearing down the testing infrastructure.
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, "testdata/crd")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows the fuzzer to create a random lowercase string within a given range
|
||||||
|
func randStringRange(f *fuzz.ConsumeFuzzer, minLen, maxLen int) (string, error) {
|
||||||
|
stringLength, err := f.GetInt()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
len := stringLength % maxLen
|
||||||
|
if len < minLen {
|
||||||
|
len += minLen
|
||||||
|
}
|
||||||
|
return f.GetStringFrom(string(letterRunes), len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a namespace and returns the created object.
|
||||||
|
func createNamespaceForFuzzing(name string) (*corev1.Namespace, error) {
|
||||||
|
namespace := &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: name},
|
||||||
|
}
|
||||||
|
err := k8sClient.Create(context.Background(), namespace)
|
||||||
|
if err != nil {
|
||||||
|
return namespace, err
|
||||||
|
}
|
||||||
|
return namespace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a secret and returns the created object.
|
||||||
|
func createKubeConfigSecretForFuzzing(namespace string) (*corev1.Secret, error) {
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "kubeconfig",
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"value.yaml": kubeConfig,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := k8sClient.Create(context.Background(), secret)
|
||||||
|
if err != nil {
|
||||||
|
return secret, err
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates pseudo-random files in rootDir.
|
||||||
|
// Will create subdirs and place the files there.
|
||||||
|
// It is the callers responsibility to ensure that
|
||||||
|
// rootDir exists.
|
||||||
|
//
|
||||||
|
// Original source:
|
||||||
|
// https://github.com/AdaLogics/go-fuzz-headers/blob/9f22f86e471065b8d56861991dc885e27b1ae7de/consumer.go#L345
|
||||||
|
//
|
||||||
|
// The change assures that as long as the f buffer has
|
||||||
|
// enough length to set numberOfFiles and the first fileName,
|
||||||
|
// this is returned without errors.
|
||||||
|
// Effectively making subDir and FileContent optional.
|
||||||
|
func createFiles(f *fuzz.ConsumeFuzzer, rootDir string) error {
|
||||||
|
noOfCreatedFiles := 0
|
||||||
|
numberOfFiles, err := f.GetInt()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maxNoFiles := numberOfFiles % 10000 //
|
||||||
|
|
||||||
|
for i := 0; i <= maxNoFiles; i++ {
|
||||||
|
fileName, err := f.GetString()
|
||||||
|
if err != nil {
|
||||||
|
if noOfCreatedFiles > 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("Could not get fileName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// leave subDir empty if no more strings are available.
|
||||||
|
subDir, _ := f.GetString()
|
||||||
|
|
||||||
|
// Avoid going outside the root dir
|
||||||
|
if strings.Contains(subDir, "../") || (len(subDir) > 0 && subDir[0] == 47) || strings.Contains(subDir, "\\") {
|
||||||
|
continue // continue as this is not a permanent error
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath, err := securejoin.SecureJoin(rootDir, subDir)
|
||||||
|
if err != nil {
|
||||||
|
continue // some errors here are not permanent, so we can try again with different values
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(dirPath, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
if noOfCreatedFiles > 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("Could not create the subDir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fullFilePath, err := securejoin.SecureJoin(dirPath, fileName)
|
||||||
|
if err != nil {
|
||||||
|
continue // potentially not a permanent error
|
||||||
|
}
|
||||||
|
|
||||||
|
// leave fileContents empty if no more bytes are available,
|
||||||
|
// afterall empty files is a valid test case.
|
||||||
|
fileContents, _ := f.GetBytes()
|
||||||
|
|
||||||
|
createdFile, err := os.Create(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
createdFile.Close()
|
||||||
|
continue // some errors here are not permanent, so we can try again with different values
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createdFile.Write(fileContents)
|
||||||
|
if err != nil {
|
||||||
|
createdFile.Close()
|
||||||
|
if noOfCreatedFiles > 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("Could not write the file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdFile.Close()
|
||||||
|
noOfCreatedFiles++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
module github.com/fluxcd/kustomize-controller/tests/fuzz
|
||||||
|
// This module is used only to avoid polluting the main module
|
||||||
|
// with fuzz dependencies.
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright 2022 The Flux 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 -euxo pipefail
|
||||||
|
|
||||||
|
GOPATH="${GOPATH:-/root/go}"
|
||||||
|
GO_SRC="${GOPATH}/src"
|
||||||
|
PROJECT_PATH="github.com/fluxcd/kustomize-controller"
|
||||||
|
|
||||||
|
cd "${GO_SRC}"
|
||||||
|
|
||||||
|
# Move fuzzer to their respective directories.
|
||||||
|
# This removes dependency noises from the modules' go.mod and go.sum files.
|
||||||
|
mv "${PROJECT_PATH}/tests/fuzz/age_fuzzer.go" "${PROJECT_PATH}/internal/sops/age/"
|
||||||
|
mv "${PROJECT_PATH}/tests/fuzz/pgp_fuzzer.go" "${PROJECT_PATH}/internal/sops/pgp/"
|
||||||
|
|
||||||
|
# Some private functions within suite_test.go are extremly useful for testing.
|
||||||
|
# Instead of duplicating them here, or refactoring them away, this simply renames
|
||||||
|
# the file to make it available to "non-testing code".
|
||||||
|
# This is a temporary fix, which will cease once the implementation is migrated to
|
||||||
|
# the built-in fuzz support in golang 1.18.
|
||||||
|
cp "${PROJECT_PATH}/controllers/suite_test.go" "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
|
||||||
|
sed -i 's;KustomizationReconciler;abc.KustomizationReconciler;g' "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
|
||||||
|
sed -i 's;import (;import(\n abc "github.com/fluxcd/kustomize-controller/controllers";g' "${PROJECT_PATH}/tests/fuzz/fuzzer_helper.go"
|
||||||
|
|
||||||
|
pushd "${PROJECT_PATH}"
|
||||||
|
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
compile_go_fuzzer "${PROJECT_PATH}/internal/sops/age/" FuzzAge fuzz_age
|
||||||
|
compile_go_fuzzer "${PROJECT_PATH}/internal/sops/pgp/" FuzzPgp fuzz_pgp
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
||||||
|
pushd "${PROJECT_PATH}/tests/fuzz"
|
||||||
|
|
||||||
|
# Setup files to be embedded into controllers_fuzzer.go's testFiles variable.
|
||||||
|
mkdir -p testdata/crd
|
||||||
|
mkdir -p testdata/sops
|
||||||
|
cp ../../config/crd/bases/*.yaml testdata/crd
|
||||||
|
cp ../../controllers/testdata/sops/age.txt testdata/sops
|
||||||
|
cp ../../controllers/testdata/sops/pgp.asc testdata/sops
|
||||||
|
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
compile_go_fuzzer "${PROJECT_PATH}/tests/fuzz/" FuzzControllers fuzz_controllers
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright 2022 The Flux 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 -euxo pipefail
|
||||||
|
|
||||||
|
# run each fuzzer once to ensure they are working properly
|
||||||
|
find /out -type f -name "fuzz*" -exec echo {} -runs=1 \; | bash -e
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Flux 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 pgp
|
||||||
|
|
||||||
|
import (
|
||||||
|
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzPgp implements a fuzzer that targets functions within pgp/keysource.go.
|
||||||
|
func FuzzPgp(data []byte) int {
|
||||||
|
f := fuzz.NewConsumer(data)
|
||||||
|
masterKey := MasterKey{}
|
||||||
|
|
||||||
|
if err := f.GenerateStruct(&masterKey); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = masterKey.Encrypt(data)
|
||||||
|
_ = masterKey.EncryptIfNeeded(data)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue