Skip to content

Commit

Permalink
Set sync setting in config automatically
Browse files Browse the repository at this point in the history
Signed-off-by: Yi Rae Kim <[email protected]>
  • Loading branch information
yiraeChristineKim committed Oct 27, 2023
1 parent 8ab1797 commit 827a608
Show file tree
Hide file tree
Showing 24 changed files with 1,075 additions and 27 deletions.
42 changes: 41 additions & 1 deletion .github/workflows/ci_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,47 @@ jobs:
kubectl -n mygatekeeper wait deployment/gatekeeper-operator-controller --for condition=Available --timeout=90s
kubectl -n mygatekeeper logs deployment/gatekeeper-operator-controller -c manager -f > operator.log &
make test-e2e NAMESPACE=mygatekeeper
kubectl delete --wait namespace mygatekeeper
- name: Debug
if: ${{ failure() }}
run: |
echo "::group::Operator Logs"
cat operator.log
echo "::endgroup::"
configsync-e2e-test:
name: Run configsync e2e tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history for all tags and branches

- uses: actions/setup-go@v3
with:
go-version-file: go.mod

- name: Download binaries
run: |
make download-binaries
- name: Create K8s KinD Cluster
run: |
kind version
make test-cluster
- name: Build and Push Test Container Image to KIND node
run: |
make docker-build IMG=localhost:5000/gatekeeper-operator:$GITHUB_SHA
kind load docker-image localhost:5000/gatekeeper-operator:$GITHUB_SHA
- name: E2E Tests
run: |
make deploy-ci NAMESPACE=mygatekeeper IMG=localhost:5000/gatekeeper-operator:$GITHUB_SHA
kubectl -n mygatekeeper wait deployment/gatekeeper-operator-controller --for condition=Available --timeout=90s
kubectl -n mygatekeeper logs deployment/gatekeeper-operator-controller -c manager -f > operator.log &
make test-e2e NAMESPACE=mygatekeeper LABEL_FILTER=config
- name: Debug
if: ${{ failure() }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ testbin/*
!vendor/**/zz_generated.*

ci-tools/

.vscode/*
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ tidy: ## Run go mod tidy

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" GOFLAGS=$(GOFLAGS) go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" GOFLAGS=$(GOFLAGS) go test $(go list ./... | grep -v /test/) -coverprofile cover.out

LABEL_FILTER=gatekeeper-controller
.PHONY: test-e2e
test-e2e: e2e-dependencies generate fmt vet ## Run e2e tests, using the configured Kubernetes cluster in ~/.kube/config
GOFLAGS=$(GOFLAGS) USE_EXISTING_CLUSTER=true $(GINKGO) --trace --fail-fast --label-filter="$(LABEL_FILTER)" ./test/e2e -- --namespace="$(NAMESPACE)" --timeout="5m" --delete-timeout="10m"
Expand All @@ -163,6 +164,17 @@ download-binaries: kustomize go-bindata envtest controller-gen
curl -sSLO https://github.com/bats-core/bats-core/archive/v${BATS_VERSION}.tar.gz && tar -zxvf v${BATS_VERSION}.tar.gz && bash bats-core-${BATS_VERSION}/install.sh $(PWD)/ci-tools
rm -rf bats-core-${BATS_VERSION} v${BATS_VERSION}.tar.gz

DEV_IMG=localhost:5000/gatekeeper-operator:dev
.PHONY: kind-bootstrap-cluster
kind-bootstrap-cluster: test-cluster dev-build
kind load docker-image $(DEV_IMG)
$(MAKE) deploy-ci NAMESPACE=$(NAMESPACE) IMG=$(DEV_IMG)
kubectl -n $(NAMESPACE) wait deployment/gatekeeper-operator-controller --for condition=Available --timeout=90s

.PHONY: dev-build
dev-build: export DOCKER_DEFAULT_PLATFORM=linux/amd64
dev-build: ## Build docker image with the manager for Mac user
$(DOCKER) build --build-arg GOOS=linux --build-arg GOARCH=amd64 --build-arg LDFLAGS=${LDFLAGS} -t ${DEV_IMG} .
##@ Build

.PHONY: build
Expand Down
7 changes: 4 additions & 3 deletions api/v1alpha1/gatekeeper_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,13 @@ const (
LogLevelError LogLevelMode = "ERROR"
)

// +kubebuilder:validation:Enum:=Enabled;Disabled
// +kubebuilder:validation:Enum:=Enabled;Disabled;Automatic
type AuditFromCacheMode string

const (
AuditFromCacheEnabled AuditFromCacheMode = "Enabled"
AuditFromCacheDisabled AuditFromCacheMode = "Disabled"
AuditFromCacheEnabled AuditFromCacheMode = "Enabled"
AuditFromCacheDisabled AuditFromCacheMode = "Disabled"
AuditFromCacheAutomatic AuditFromCacheMode = "Automatic"
)

// +kubebuilder:validation:Enum:=Enabled;Disabled
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ var (

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme

GatekeeperGVR = schema.GroupVersionResource{
Group: GroupVersion.Group,
Version: GroupVersion.Version,
Resource: "gatekeeper",
}
)
1 change: 1 addition & 0 deletions bundle/manifests/operator.gatekeeper.sh_gatekeepers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ spec:
enum:
- Enabled
- Disabled
- Automatic
type: string
auditInterval:
type: string
Expand Down
1 change: 1 addition & 0 deletions config/crd/bases/operator.gatekeeper.sh_gatekeepers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ spec:
enum:
- Enabled
- Disabled
- Automatic
type: string
auditInterval:
type: string
Expand Down
260 changes: 260 additions & 0 deletions controllers/constraintstatus_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package controllers

import (
"context"
"strings"
"time"

"github.com/go-logr/logr"
"github.com/open-policy-agent/gatekeeper/v3/apis/config/v1alpha1"
"github.com/open-policy-agent/gatekeeper/v3/apis/status/v1beta1"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/utils/strings/slices"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var ControllerName = "contraintstatus_reconciler"

type discoveryInfo struct {
apiResourceList []*metav1.APIResourceList
discoveryLastRefreshed time.Time
}

type ConstraintStatusReconciler struct {
client.Client
Log logr.Logger
DynamicClient dynamic.Interface
ClientSet *kubernetes.Clientset
Scheme *runtime.Scheme
Namespace string
MaxConcurrentReconciles uint
discoveryInfo
}

// SetupWithManager sets up the controller with the Manager.
func (r *ConstraintStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
// The work queue prevents the same item being reconciled concurrently:
// https://github.com/kubernetes-sigs/controller-runtime/issues/1416#issuecomment-899833144
WithOptions(controller.Options{MaxConcurrentReconciles: int(r.MaxConcurrentReconciles)}).
// watch Config resrouce as secondary
Named(ControllerName).
For(&v1beta1.ConstraintPodStatus{},
builder.WithPredicates(predicate.Funcs{
// Execute this reconcile func when it is audit-constraintStatuspod
// because a constraint creates 4 constraintStatuspods
CreateFunc: func(e event.CreateEvent) bool {
obj := e.Object.(*v1beta1.ConstraintPodStatus)

return slices.Contains(obj.Status.Operations, "audit")
},
UpdateFunc: func(e event.UpdateEvent) bool {
oldObj := e.ObjectOld.(*v1beta1.ConstraintPodStatus)
newObj := e.ObjectNew.(*v1beta1.ConstraintPodStatus)

return slices.Contains(newObj.Status.Operations, "audit") &&
oldObj.Status.ObservedGeneration != newObj.Status.ObservedGeneration
},
DeleteFunc: func(e event.DeleteEvent) bool {
obj := e.Object.(*v1beta1.ConstraintPodStatus)

return slices.Contains(obj.Status.Operations, "audit")
},
},
)).
Complete(r)
}

// User set gatekeeper.spec.audit.auditFromCache to Automatic, this reconcile function
// collect all constraints and add found kinds which is used in contraint to config.spec.sync.syncOnly
func (r *ConstraintStatusReconciler) Reconcile(ctx context.Context,
request reconcile.Request,
) (reconcile.Result, error) {
log := r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
log.Info("Reconciling ConstraintPodStatus and Config")

// Get config or create if not exist
config := &v1alpha1.Config{}
err := r.Get(ctx, types.NamespacedName{
Namespace: r.Namespace,
Name: "config",
}, config)

if apierrors.IsNotFound(err) {
config = &v1alpha1.Config{
ObjectMeta: metav1.ObjectMeta{
Name: "config",
Namespace: r.Namespace,
},
}
err = r.Create(ctx, config)

if err != nil {
return reconcile.Result{}, err
}
}

contraintPodStatus := &v1beta1.ConstraintPodStatus{}
err = r.Get(ctx, request.NamespacedName, contraintPodStatus)

if err != nil {
if apierrors.IsNotFound(err) {
log.Error(err, "Cannot find any contraintStatus")

return reconcile.Result{}, nil
}
// Requeue
return reconcile.Result{}, err
}

labels := contraintPodStatus.GetLabels()

constraintKind := labels["internal.gatekeeper.sh/constraint-kind"]
constraintName := labels["internal.gatekeeper.sh/constraint-name"]

table := map[v1alpha1.SyncOnlyEntry]bool{}
// Add to table for uniqueue filtering
for _, entry := range config.Spec.Sync.SyncOnly {
table[entry] = true
}

constraintGVR := schema.GroupVersionResource{
Group: "constraints.gatekeeper.sh",
Version: "v1beta1",
Resource: strings.ToLower(constraintKind),
}

constraint, err := r.DynamicClient.Resource(constraintGVR).Get(ctx, constraintName, metav1.GetOptions{})
if err != nil {
return reconcile.Result{}, err
}

constraintMatchKinds, _, err := unstructured.NestedSlice(constraint.Object, "spec", "match", "kinds")
if err != nil {
return reconcile.Result{}, err
}

contraintSyncOnlyEntries, err := r.getSyncOnlys(constraintMatchKinds)
if err != nil {
log.Error(err, "Error to get matching kind and apigroup")

return reconcile.Result{}, nil
}

for _, ce := range *contraintSyncOnlyEntries {
table[ce] = true
}

syncOnlys := []v1alpha1.SyncOnlyEntry{}
for key := range table {
syncOnlys = append(syncOnlys, key)
}

config.Spec.Sync.SyncOnly = syncOnlys
err = r.Update(ctx, config, &client.UpdateOptions{})

if err != nil {
return reconcile.Result{}, err
}

return reconcile.Result{}, nil
}

func (r *ConstraintStatusReconciler) getSyncOnlys(constraintMatchKinds []interface{}) (
*[]v1alpha1.SyncOnlyEntry, error,
) {
syncOnlys := []v1alpha1.SyncOnlyEntry{}

for _, kind := range constraintMatchKinds {
newKind := kind.(map[string]interface{})
apiGroups := newKind["apiGroups"].([]interface{})
kindsInKinds := newKind["kinds"].([]interface{})

for _, apiGroup := range apiGroups {
for _, kindkind := range kindsInKinds {
version, err := r.getAPIVersion(kindkind.(string), apiGroup.(string))
if err != nil {
return nil, err
}

syncOnlys = append(syncOnlys, v1alpha1.SyncOnlyEntry{
Group: apiGroup.(string),
Version: version,
Kind: kindkind.(string),
})
}
}
}

return &syncOnlys, nil
}

func (r *ConstraintStatusReconciler) getAPIVersion(kind string, apiGroup string) (string, error) {
// cool time(10 min) to refresh discoveries
if len(r.apiResourceList) == 0 ||
r.discoveryLastRefreshed.Add(time.Minute*10).Before(time.Now()) {
err := r.refreshDiscoveryInfo()
r.discoveryLastRefreshed = time.Now()

if err != nil {
return "", err
}
}

for _, resc := range r.apiResourceList {
groupVerison, err := schema.ParseGroupVersion(resc.GroupVersion)
if err != nil {
r.Log.Error(err, "Cannot Parse Group and version in getApiVersion")

return "", errors.New("Error in parse Group and version in getApiVersion function")
}

group := groupVerison.Group
version := groupVerison.Version
// Consider groupversion == v1 or groupversion == app1/v1
for _, apiResource := range resc.APIResources {
if apiResource.Kind == kind && group == apiGroup {
return version, nil
}
}
}

// Get new discoveryInfo, when any resource is not found
err := r.refreshDiscoveryInfo()
r.discoveryLastRefreshed = time.Now()

if err != nil {
return "", err
}

return "", errors.New("Getting discovery has error")
}

// Retrieve all groups and versions to add in config sync
// Constraints present only kind and group so this function helps to find the version
func (r *ConstraintStatusReconciler) refreshDiscoveryInfo() error {
discoveryClient := r.ClientSet.Discovery()

apiList, err := discoveryClient.ServerPreferredResources()
if err != nil {
return err
}

r.apiResourceList = apiList

return nil
}
1 change: 1 addition & 0 deletions deploy/gatekeeper-operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,7 @@ spec:
enum:
- Enabled
- Disabled
- Automatic
type: string
auditInterval:
type: string
Expand Down
Loading

0 comments on commit 827a608

Please sign in to comment.