diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml index 12d6ddc1..b31bbb98 100644 --- a/.github/workflows/kind.yml +++ b/.github/workflows/kind.yml @@ -104,17 +104,6 @@ jobs: name: coverage_e2e path: coverage_e2e.out - - name: E2E Tests for Compliance Events API - run: | - KUBECONFIG=${PWD}/kubeconfig_hub make e2e-test-coverage-compliance-events-api - - - name: Upload Compliance Events API Test Coverage - if: ${{ github.event_name == 'pull_request' && matrix.kind == 'latest'}} - uses: actions/upload-artifact@v4 - with: - name: coverage_e2e_compliance_events_api - path: coverage_e2e_compliance_events_api.out - - name: Verify Deployment Configuration run: | make webhook @@ -220,11 +209,6 @@ jobs: with: name: coverage_e2e - - name: Download Compliance Events Coverage Result - uses: actions/download-artifact@v4 - with: - name: coverage_e2e_compliance_events_api - - name: Download PolicyAutomation Coverage Result uses: actions/download-artifact@v4 with: diff --git a/.vscode/launch.json b/.vscode/launch.json index 10557aa3..033eb863 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,8 +1,6 @@ { "version": "0.2.0", "configurations": [ - // If you are running the compliance API tests (case 20) with this, pass the `--compliance-api-port=8385` to - // Ginkgo. { "name": "Launch Controller", "type": "go", @@ -14,14 +12,10 @@ "--log-level=4", "-v=4", "--enable-webhooks=false", - "--compliance-history-api-port=8385", - "--compliance-history-api-cert=dev-tls.crt", - "--compliance-history-api-key=dev-tls.key" ], "env": { "WATCH_NAMESPACE": "", "KUBECONFIG": "${workspaceFolder}/kubeconfig_hub", - "WATCH_NAMESPACE_COMPLIANCE_EVENTS_STORE": "open-cluster-management" } }, // Set FDescribe or FIt on the test to debug. Then set the desired breakpoint. diff --git a/Makefile b/Makefile index 60bacfcd..d2f5e03e 100644 --- a/Makefile +++ b/Makefile @@ -28,10 +28,9 @@ CONTROLLER_NAMESPACE ?= open-cluster-management # Handle KinD configuration CLUSTER_NAME ?= hub KIND_NAMESPACE ?= $(CONTROLLER_NAMESPACE) -POSTGRES_HOST ?= localhost # Test coverage threshold -export COVERAGE_MIN ?= 74 +export COVERAGE_MIN ?= 71 # Image URL to use all building/pushing image targets; # Use your own docker registry and image name for dev/test by overridding the IMG and REGISTRY environment variable. @@ -143,11 +142,10 @@ generate-operator-yaml: kustomize manifests ############################################################ .PHONY: kind-bootstrap-cluster -kind-bootstrap-cluster: POSTGRES_HOST=postgres kind-bootstrap-cluster: kind-bootstrap-cluster-dev webhook kind-deploy-controller install-resources .PHONY: kind-bootstrap-cluster-dev -kind-bootstrap-cluster-dev: kind-create-cluster install-crds kind-controller-kubeconfig postgres +kind-bootstrap-cluster-dev: kind-create-cluster install-crds kind-controller-kubeconfig cert-manager: @echo Installing cert-manager @@ -156,29 +154,6 @@ cert-manager: kubectl wait deployment -n cert-manager cert-manager --for condition=Available=True --timeout=180s kubectl wait --for=condition=Ready pod -l app.kubernetes.io/instance=cert-manager -n cert-manager --timeout=180s -postgres: cert-manager - @echo "Installing Postgres" - -kubectl create ns $(KIND_NAMESPACE) - sed 's/open-cluster-management/$(KIND_NAMESPACE)/g' build/kind/postgres.yaml | kubectl apply --timeout=180s -f- - - @echo "Waiting until the pods are up" - @sleep 3 - kubectl -n $(KIND_NAMESPACE) wait --for=condition=Ready pod -l app=postgres - - @echo "Creating the governance-policy-database secret" - @kubectl -n $(KIND_NAMESPACE) get secret governance-policy-database || \ - kubectl -n $(KIND_NAMESPACE) create secret generic governance-policy-database \ - --from-literal="user=grc" \ - --from-literal="password=grc" \ - --from-literal="host=$(POSTGRES_HOST)" \ - --from-literal="dbname=ocm-compliance-history" \ - --from-literal="ca=$$(kubectl -n $(KIND_NAMESPACE) get secret postgres-cert -o json | jq -r '.data["ca.crt"]' | base64 -d)" - - @echo "Copying the compliance API certificates locally" - kubectl -n $(KIND_NAMESPACE) get secret compliance-api-cert -o json | jq -r '.data["tls.crt"]' | base64 -d > dev-tls.crt - kubectl -n $(KIND_NAMESPACE) get secret compliance-api-cert -o json | jq -r '.data["ca.crt"]' | base64 -d >> dev-ca.crt - kubectl -n $(KIND_NAMESPACE) get secret compliance-api-cert -o json | jq -r '.data["tls.key"]' | base64 -d > dev-tls.key - webhook: cert-manager -kubectl create ns $(KIND_NAMESPACE) sed -E 's,open-cluster-management(.svc|/|$$),$(KIND_NAMESPACE)\1,g' deploy/webhook.yaml | kubectl apply -f - @@ -207,10 +182,6 @@ kind-deploy-controller-dev: kind-deploy-controller kubectl rollout restart deployment/$(IMG) -n $(KIND_NAMESPACE) kubectl rollout status -n $(KIND_NAMESPACE) deployment $(IMG) --timeout=180s -# Specify KIND_VERSION to indicate the version tag of the KinD image -.PHONY: kind-create-cluster -kind-create-cluster: KIND_ARGS += --config build/kind/kind-config.yaml - .PHONY: kind-delete-cluster kind-delete-cluster: kind delete cluster --name $(KIND_NAME) @@ -256,7 +227,7 @@ install-resources: @echo setting a Hub cluster DNS name kubectl apply -f test/resources/case5_policy_automation/cluster-dns.yaml -E2E_LABEL_FILTER = --label-filter="!webhook && !compliance-events-api && !policyautomation" +E2E_LABEL_FILTER = --label-filter="!webhook && !policyautomation" .PHONY: e2e-test e2e-test: e2e-dependencies $(GINKGO) -v --fail-fast $(E2E_TEST_ARGS) $(E2E_LABEL_FILTER) test/e2e -- $(E2E_TEST_CODE_ARGS) @@ -265,14 +236,6 @@ e2e-test: e2e-dependencies e2e-test-webhook: E2E_LABEL_FILTER = --label-filter="webhook" e2e-test-webhook: e2e-test -.PHONY: e2e-test-compliance-events-api -e2e-test-compliance-events-api: E2E_LABEL_FILTER = --label-filter="compliance-events-api" -e2e-test-compliance-events-api: e2e-test - -.PHONY: e2e-test-coverage-compliance-events-api -e2e-test-coverage-compliance-events-api: E2E_TEST_ARGS = --json-report=report_e2e_compliance_events_api.json --covermode=atomic --coverpkg=open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi --coverprofile=coverage_e2e_compliance_events_api.out --output-dir=. -e2e-test-coverage-compliance-events-api: e2e-test-compliance-events-api - .PHONY: e2e-test-policyautomation e2e-test-policyautomation: E2E_LABEL_FILTER = --label-filter="policyautomation" e2e-test-policyautomation: e2e-test @@ -296,7 +259,6 @@ e2e-stop-instrumented: .PHONY: e2e-test-coverage e2e-test-coverage: E2E_TEST_ARGS = --json-report=report_e2e.json --output-dir=. -e2e-test-coverage: E2E_TEST_CODE_ARGS = --compliance-api-port=8385 e2e-test-coverage: e2e-run-instrumented e2e-test e2e-stop-instrumented .PHONY: e2e-test-coverage-policyautomation diff --git a/README.md b/README.md index 8c760ccc..786d4a43 100644 --- a/README.md +++ b/README.md @@ -85,26 +85,6 @@ in particular, the details in `./deploy/manager/manager.yaml`. When any of those deployment yaml `./deploy/operator.yaml` must be regenerated through the `make generate-operator-yaml` target. The `./deploy/operator.yaml` SHOULD NOT be manually updated. -## Running the Compliance Events API - -Create the KinD cluster and install Postgres with the following commands: - -```bash -make kind-bootstrap-cluster-dev -``` - -You can connect to the Postgres server with the following command: - -```bash -psql "host=localhost dbname=ocm-compliance-history user=grc password=grc" -``` - -Run the Governance Policy Propagator with the following command: - -```bash -WATCH_NAMESPACE="" WATCH_NAMESPACE_COMPLIANCE_EVENTS_STORE="open-cluster-management" go run main.go --leader-elect=false --enable-webhooks=false -``` - ## References - The `governance-policy-propagator` is part of the `open-cluster-management` community. For more information, visit: diff --git a/build/common/config/.golangci.yml b/build/common/config/.golangci.yml index 7e340f0e..f838ae51 100644 --- a/build/common/config/.golangci.yml +++ b/build/common/config/.golangci.yml @@ -223,7 +223,7 @@ issues: # Disable the lint failure about the name "stuttering" - linters: - revive - source: type (PolicyStatusReconciler|ComplianceDBSecretReconciler) struct + source: type (PolicyStatusReconciler) struct # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all diff --git a/build/kind/kind-config.yaml b/build/kind/kind-config.yaml deleted file mode 100644 index 71a62528..00000000 --- a/build/kind/kind-config.yaml +++ /dev/null @@ -1,10 +0,0 @@ -kind: Cluster -apiVersion: kind.x-k8s.io/v1alpha4 -nodes: -- role: control-plane - # Expose Postgres - extraPortMappings: - - containerPort: 30543 - hostPort: 5432 - - containerPort: 30838 - hostPort: 8384 diff --git a/build/kind/postgres.yaml b/build/kind/postgres.yaml deleted file mode 100644 index ab279a83..00000000 --- a/build/kind/postgres.yaml +++ /dev/null @@ -1,158 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: postgres-selfsigned-issuer - namespace: open-cluster-management -spec: - selfSigned: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: postgres-external - namespace: open-cluster-management - labels: - app: postgres -spec: - selector: - app: postgres - ports: - - name: postgres-external - port: 5432 - targetPort: 5432 - nodePort: 30543 - type: NodePort ---- -apiVersion: v1 -kind: Service -metadata: - name: postgres - namespace: open-cluster-management - labels: - app: postgres -spec: - selector: - app: postgres - ports: - - name: postgres - port: 5432 - targetPort: 5432 ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: postgres-cert - namespace: open-cluster-management -spec: - dnsNames: - - postgres-external.open-cluster-management.svc - - postgres-external.open-cluster-management.svc.cluster.local - - postgres - - localhost - issuerRef: - kind: Issuer - name: postgres-selfsigned-issuer - secretName: postgres-cert ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres - namespace: open-cluster-management -spec: - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - securityContext: - # This is the postgres group. - fsGroup: 999 - containers: - - name: postgres - command: - - docker-entrypoint.sh - - -c - - ssl=on - - -c - - ssl_cert_file=/var/lib/postgresql/tls/tls.crt - - -c - - ssl_key_file=/var/lib/postgresql/tls/tls.key - - -c - - ssl_ca_file=/var/lib/postgresql/tls/ca.crt - - -c - - log_statement=all - - -c - - log_destination=stderr - # This is a mirror of postgres:13 on Docker Hub to avoid rate limits. - image: quay.io/stolostron/grc-ci-postgres:13 - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 - env: - - name: POSTGRES_PASSWORD - value: grc - - name: POSTGRES_USER - value: grc - - name: POSTGRES_DB - value: ocm-compliance-history - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: postgres-db - subPath: data - - mountPath: /var/lib/postgresql/tls - name: postgres-cert - readOnly: true - volumes: - - name: postgres-cert - secret: - secretName: postgres-cert - # Postgres requires limited permissions on the private key. - defaultMode: 0o440 - - name: postgres-db - emptyDir: - sizeLimit: 250Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: compliance-api-external - namespace: open-cluster-management - labels: - app: compliance-api -spec: - selector: - name: governance-policy-propagator - ports: - - name: compliance-api-external - port: 8384 - targetPort: 8384 - nodePort: 30838 - type: NodePort ---- -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: compliance-api-selfsigned-issuer - namespace: open-cluster-management -spec: - selfSigned: {} ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: compliance-api-cert - namespace: open-cluster-management -spec: - dnsNames: - - compliance-api-external.open-cluster-management.svc - - compliance-api-external.open-cluster-management.svc.cluster.local - - localhost - issuerRef: - kind: Issuer - name: compliance-api-selfsigned-issuer - secretName: compliance-api-cert diff --git a/controllers/complianceeventsapi/auth.go b/controllers/complianceeventsapi/auth.go deleted file mode 100644 index 7fe00a30..00000000 --- a/controllers/complianceeventsapi/auth.go +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright Contributors to the Open Cluster Management project -package complianceeventsapi - -import ( - "encoding/base64" - "encoding/json" - "net/http" - "slices" - "strings" - - "github.com/stolostron/rbac-api-utils/pkg/rbac" - authzv1 "k8s.io/api/authorization/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -func getManagedClusterRules(userChangedConfig *rest.Config, managedClusterNames []string, -) (map[string][]string, error) { - kclient, err := kubernetes.NewForConfig(userChangedConfig) - if err != nil { - log.Error(err, "Failed to create a Kubernetes client with the user token") - - return nil, err - } - - managedClusterGR := schema.GroupResource{ - Group: "cluster.open-cluster-management.io", - Resource: "managedclusters", - } - - // key is managedClusterName and value is verbs. - // ex: {"managed1": ["get"], "managed2": ["list","create"], "managed3": []} - return rbac.GetResourceAccess(kclient, managedClusterGR, managedClusterNames, "") -} - -func canGetManagedCluster(userChangedConfig *rest.Config, managedClusterName string, -) (bool, error) { - allRules, err := getManagedClusterRules(userChangedConfig, []string{managedClusterName}) - if err != nil { - return false, err - } - - return getAccessByClusterName(allRules, managedClusterName), nil -} - -func getAccessByClusterName(allManagedClusterRules map[string][]string, clusterName string) bool { - starRules, ok := allManagedClusterRules["*"] - if ok && slices.Contains(starRules, "get") || slices.Contains(starRules, "*") { - return true - } - - rules, ok := allManagedClusterRules[clusterName] - if ok && slices.Contains(rules, "get") || slices.Contains(rules, "*") { - return true - } - - return false -} - -// parseToken will return the token string in the Authorization header. -func parseToken(req *http.Request) string { - return strings.TrimSpace(strings.TrimPrefix(req.Header.Get("Authorization"), "Bearer")) -} - -// canRecordComplianceEvent will perform token authentication and perform a self subject access review to -// ensure the input user has patch access to patch the policy status in the managed cluster namespace. An error is -// returned if the authorization could not be determined. -func canRecordComplianceEvent(cfg *rest.Config, clusterName string, req *http.Request) (bool, error) { - userConfig, err := getUserKubeConfig(cfg, req) - if err != nil { - return false, err - } - - userClient, err := kubernetes.NewForConfig(userConfig) - if err != nil { - return false, err - } - - result, err := userClient.AuthorizationV1().SelfSubjectAccessReviews().Create( - req.Context(), - &authzv1.SelfSubjectAccessReview{ - Spec: authzv1.SelfSubjectAccessReviewSpec{ - ResourceAttributes: &authzv1.ResourceAttributes{ - Group: "policy.open-cluster-management.io", - Version: "v1", - Resource: "policies", - Verb: "patch", - Namespace: clusterName, - Subresource: "status", - }, - }, - }, - metav1.CreateOptions{}, - ) - if err != nil { - if k8serrors.IsUnauthorized(err) { - return false, ErrUnauthorized - } - - return false, err - } - - if !result.Status.Allowed { - log.V(0).Info( - "The user is not authorized to record a compliance event", - "cluster", clusterName, - "user", getTokenUsername(userConfig.BearerToken), - ) - } - - return result.Status.Allowed, nil -} - -// getTokenUsername will parse the token and return the username. If the token is invalid, an empty string is returned. -func getTokenUsername(token string) string { - parts := strings.Split(token, ".") - if len(parts) != 3 { - log.V(2).Info("The token does not have the expected three parts") - - return "" - } - - userInfoBytes, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { - log.V(2).Info("The token does not have valid base64") - - return "" - } - - userInfo := map[string]interface{}{} - - err = json.Unmarshal(userInfoBytes, &userInfo) - if err != nil { - log.V(2).Info("The token does not have valid JSON") - - return "" - } - - username, ok := userInfo["sub"].(string) - if !ok { - return "" - } - - return username -} - -func getUserKubeConfig(config *rest.Config, r *http.Request) (*rest.Config, error) { - userConfig := &rest.Config{ - Host: config.Host, - APIPath: config.APIPath, - TLSClientConfig: rest.TLSClientConfig{ - CAFile: config.TLSClientConfig.CAFile, - CAData: config.TLSClientConfig.CAData, - ServerName: config.TLSClientConfig.ServerName, - // For testing - Insecure: config.TLSClientConfig.Insecure, - }, - } - - userConfig.BearerToken = parseToken(r) - - if userConfig.BearerToken == "" { - return nil, ErrUnauthorized - } - - return userConfig, nil -} diff --git a/controllers/complianceeventsapi/auth_test.go b/controllers/complianceeventsapi/auth_test.go deleted file mode 100644 index 4b3f5d33..00000000 --- a/controllers/complianceeventsapi/auth_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package complianceeventsapi - -import ( - "testing" -) - -func TestGetTokenUsername(t *testing.T) { - t.Parallel() - - token := "part1.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc" + - "3BhY2UiOiJvcGVuLWNsdXN0ZXItbWFuYWdlbWVudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJnb3Zl" + - "cm5hbmNlLXBvbGljeS1wcm9wYWdhdG9yIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Imdv" + - "dmVybmFuY2UtcG9saWN5LXByb3BhZ2F0b3IiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJ" + - "lMjQzZDBlNi03YjJkLTRjZjQtYmExMC1mMTE5NWQwMGUxZTYiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b3Blbi1jbHVzdGVyLW" + - "1hbmFnZW1lbnQ6Z292ZXJuYW5jZS1wb2xpY3ktcHJvcGFnYXRvciJ9.part3" - - username := getTokenUsername(token) - expected := "system:serviceaccount:open-cluster-management:governance-policy-propagator" - - if username != expected { - t.Fatalf("Expected %s but got %s", expected, username) - } -} diff --git a/controllers/complianceeventsapi/complianceeventsapi_controller.go b/controllers/complianceeventsapi/complianceeventsapi_controller.go deleted file mode 100644 index 5cd4d4da..00000000 --- a/controllers/complianceeventsapi/complianceeventsapi_controller.go +++ /dev/null @@ -1,523 +0,0 @@ -// Copyright Contributors to the Open Cluster Management project - -package complianceeventsapi - -import ( - "context" - "database/sql" - "embed" - "errors" - "fmt" - "net/url" - "os" - "path" - "strings" - "sync" - "time" - - "github.com/golang-migrate/migrate/v4" - // Required to activate the Postgres driver - _ "github.com/golang-migrate/migrate/v4/database/postgres" - "github.com/golang-migrate/migrate/v4/source" - "github.com/golang-migrate/migrate/v4/source/iofs" - k8sdepwatches "github.com/stolostron/kubernetes-dependency-watches/client" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8sruntime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/util/workqueue" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - policyv1 "open-cluster-management.io/governance-policy-propagator/api/v1" - "open-cluster-management.io/governance-policy-propagator/controllers/common" -) - -//go:embed migrations -var migrationsFS embed.FS - -const ( - ControllerName = "compliance-events-api" - DBSecretName = "governance-policy-database" - WatchNamespaceEnvVar = "WATCH_NAMESPACE_COMPLIANCE_EVENTS_STORE" -) - -var ( - log = ctrl.Log.WithName(ControllerName) - ErrInvalidDBSecret = errors.New("the governance-policy-database secret is invalid") - ErrInvalidConnectionURL = errors.New("the database connection URL is invalid") - ErrDBConnectionFailed = errors.New("the compliance events database could not be connected to") - migrationsSource source.Driver - gvkSecret = schema.GroupVersionKind{Version: "v1", Kind: "Secret"} - ErrRetryable = errors.New("") -) - -func init() { - var err error - migrationsSource, err = iofs.New(migrationsFS, "migrations") - - utilruntime.Must(err) -} - -// ComplianceServerCtx acts as a "global" database instance that all required controllers share. The -// ComplianceDBSecretReconciler reconciler is responsible for updating the DB field if the connection info gets added -// or changes. MonitorDatabaseConnection will periodically check the health of the database connection and monitor -// the Queue. See MonitorDatabaseConnection for more information. -type ComplianceServerCtx struct { - // A write lock is used when the database connection changes and the DB object needs to be replaced. - // A read lock should be used when the DB is accessed. - Lock sync.RWMutex - DB *sql.DB - Queue workqueue.TypedInterface[types.NamespacedName] - needsMigration bool - // Required to run a migration after the database connection changed or the feature was enabled. - connectionURL string - // These caches get reset after a database migration due to a connection drop and reconnect. - ParentPolicyToID sync.Map - PolicyToID sync.Map - ClusterID string -} - -// NewComplianceServerCtx returns a ComplianceServerCtx with initialized values. It does not start a connection -// but does validate the connection URL for syntax. If the connection URL is not provided or is invalid, -// ErrInvalidConnectionURL is returned. -func NewComplianceServerCtx(dbConnectionURL string, clusterID string) (*ComplianceServerCtx, error) { - var db *sql.DB - var err error - - if dbConnectionURL == "" { - err = ErrInvalidConnectionURL - } else { - var openErr error - // As of the writing of this code, sql.Open doesn't create a connection. db.Ping will though, so this - // should never fail unless the connection URL is invalid to the Postgres driver. - db, openErr = sql.Open("postgres", dbConnectionURL) - if openErr != nil { - err = fmt.Errorf("%w: %w", ErrInvalidConnectionURL, err) - } - } - - return &ComplianceServerCtx{ - Lock: sync.RWMutex{}, - Queue: workqueue.NewTyped[types.NamespacedName](), - connectionURL: dbConnectionURL, - DB: db, - ClusterID: clusterID, - }, err -} - -// ComplianceDBSecretReconciler is responsible for managing the compliance events history database migrations and -// keeping the shared database connection up to date. -type ComplianceDBSecretReconciler struct { - DynamicWatcher k8sdepwatches.DynamicWatcher - Client *kubernetes.Clientset - // TempDir is used for temporary files such as a custom CA to use to verify the Postgres TLS connection. The - // caller is responsible for cleaning it up after the controller stops. - TempDir string - ConnectionURL string - ComplianceServerCtx *ComplianceServerCtx -} - -// WARNING: In production, this should be namespaced to the namespace the controller is running in. -//+kubebuilder:rbac:groups=core,resources=secrets,resourceNames=governance-policy-database,verbs=get;list;watch -//+kubebuilder:rbac:groups=core,resources=events,verbs=create -//+kubebuilder:rbac:groups=authorization.k8s.io,resources=subjectaccessreviews,verbs=create - -// Reconcile watches the governance-policy-database secret in the controller namespace. On updates it'll trigger -// a database migration and update the shared database connection. -func (r *ComplianceDBSecretReconciler) Reconcile( - ctx context.Context, watcher k8sdepwatches.ObjectIdentifier, -) (ctrl.Result, error) { - log := log.WithValues("secretNamespace", watcher.Namespace, "secret", watcher.Name) - log.Info("Reconciling a Secret") - - // The watch configuration should prevent this from happening, but add this as a precaution. - if watcher.Name != DBSecretName { - log.Info("Got a reconciliation request for an unexpected Secret. This should have been filtered out.") - - return reconcile.Result{}, nil - } - - var parsedConnectionURL string - - dbSecret, err := r.DynamicWatcher.GetFromCache(gvkSecret, watcher.Namespace, watcher.Name) - if dbSecret == nil || errors.Is(err, k8sdepwatches.ErrNoCacheEntry) { - parsedConnectionURL = "" - } else if err != nil { - return reconcile.Result{}, err - } else { - var typedDBSecret corev1.Secret - - err := k8sruntime.DefaultUnstructuredConverter.FromUnstructured(dbSecret.UnstructuredContent(), &typedDBSecret) - if err != nil { - log.Error(err, "The cached database secret could not be converted to a typed secret") - - return reconcile.Result{}, nil - } - - parsedConnectionURL, err = ParseDBSecret(&typedDBSecret, r.TempDir) - if errors.Is(err, ErrInvalidDBSecret) { - log.Error(err, "Will retry once the invalid secret is updated") - - parsedConnectionURL = "" - } else if err != nil { - log.Error(err, "Will retry in 30 seconds due to the error") - - return reconcile.Result{RequeueAfter: time.Second * 30}, nil - } - } - - if r.ConnectionURL != parsedConnectionURL { - log.Info( - "The database connection URL has changed. Will handle missed database entries during downtime.", - ) - - r.ConnectionURL = parsedConnectionURL - - r.ComplianceServerCtx.Lock.Lock() - defer r.ComplianceServerCtx.Lock.Unlock() - - // Need the connection URL for the migration. - r.ComplianceServerCtx.connectionURL = r.ConnectionURL - - // Clear the database ID caches in case this is a new database or the database was restored - r.ComplianceServerCtx.ParentPolicyToID = sync.Map{} - r.ComplianceServerCtx.PolicyToID = sync.Map{} - clusterKeyCache = sync.Map{} - - if parsedConnectionURL == "" { - r.ComplianceServerCtx.DB = nil - } else { - // As of the writing of this code, sql.Open doesn't create a connection. db.Ping will though, so this - // should never fail unless the connection URL is invalid to the Postgres driver. - db, err := sql.Open("postgres", r.ConnectionURL) - if err != nil { - log.Error( - err, - "The Postgres connection URL could not be parsed by the driver. Try updating the secret.", - ) - } - - // This may be nil and that is intentional. - r.ComplianceServerCtx.DB = db - // Once the connection URL changes, a migration is required in the event this is a new database or the - // propagator was started when the database was offline and it was not up to date. - // If the migration fails, let MonitorDatabaseConnection handle it. - _ = r.ComplianceServerCtx.MigrateDB(ctx, r.Client, watcher.Namespace) - } - } - - return reconcile.Result{}, nil -} - -// MonitorDatabaseConnection will check the database connection health every 20 seconds. If healthy, it will migrate -// the database if necessary, and send any reconcile requests to the replicated policy controller from -// complianceServerCtx.Queue. To stop MonitorDatabaseConnection, cancel the input context. -func MonitorDatabaseConnection( - ctx context.Context, - complianceServerCtx *ComplianceServerCtx, - client *kubernetes.Clientset, - controllerNamespace string, - reconcileRequests chan<- event.GenericEvent, -) { - for { - sleep, cancelSleep := context.WithTimeout(context.Background(), time.Second*20) - - log.V(3).Info("Sleeping for 20 seconds until the next database check") - - select { - case <-ctx.Done(): - complianceServerCtx.Queue.ShutDown() - cancelSleep() - - return - case <-sleep.Done(): - // Satisfy the linter, but in reality, this is a noop. - cancelSleep() - } - - complianceServerCtx.Lock.RLock() - - if complianceServerCtx.DB == nil { - complianceServerCtx.Lock.RUnlock() - - continue - } - - if !complianceServerCtx.needsMigration && complianceServerCtx.Queue.Len() == 0 { - complianceServerCtx.Lock.RUnlock() - - continue - } - - if err := complianceServerCtx.DB.PingContext(ctx); err != nil { - complianceServerCtx.Lock.RUnlock() - - log.Info("The database connection failed: " + err.Error()) - - continue - } - - if complianceServerCtx.needsMigration { - // Upgrade to a write lock to migrate the database. - complianceServerCtx.Lock.RUnlock() - complianceServerCtx.Lock.Lock() - err := complianceServerCtx.MigrateDB(ctx, client, controllerNamespace) - complianceServerCtx.Lock.Unlock() - - if err != nil { - continue - } - - // Reinitate the read lock and ensure the DB is still defined after obtaining the lock. - complianceServerCtx.Lock.RLock() - - if complianceServerCtx.DB == nil { - complianceServerCtx.Lock.RUnlock() - - continue - } - } - - log.V(3).Info( - "The compliance database is up. Checking for queued up reconcile requests.", - "queueLength", complianceServerCtx.Queue.Len(), - ) - - sendLogMsg := complianceServerCtx.Queue.Len() > 0 - - for complianceServerCtx.Queue.Len() > 0 { - request, shutdown := complianceServerCtx.Queue.Get() - - reconcileRequests <- event.GenericEvent{ - Object: &common.GuttedObject{ - TypeMeta: metav1.TypeMeta{ - APIVersion: policyv1.GroupVersion.String(), - Kind: "Policy", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: request.Name, - Namespace: request.Namespace, - }, - }, - } - - complianceServerCtx.Queue.Done(request) - - // The queue should never get shutdown and still reach here which is why it's an info log. We need to - // know about it if it happens unexpectedly. - if shutdown { - log.Info("The queue was shutdown. Exiting MonitorDatabaseConnection.") - - complianceServerCtx.Lock.RUnlock() - - return - } - } - - if sendLogMsg { - log.V(1).Info( - "Done sending queued reconcile requests. Sleeping for 20 seconds until the next database check.", - "queueLength", complianceServerCtx.Queue.Len(), - ) - } - - complianceServerCtx.Lock.RUnlock() - } -} - -// MigrateDB will perform a database migration if required and send Kubernetes events if the migration fails. -// ErrDBConnectionFailed will be returned if the database connection failed. Obtain a write lock before calling -// this method if multiple goroutines use this ComplianceServerCtx instance. -func (c *ComplianceServerCtx) MigrateDB( - ctx context.Context, client *kubernetes.Clientset, controllerNamespace string, -) error { - c.needsMigration = true - - if c.connectionURL == "" { - return fmt.Errorf("%w: the connection URL is not set", ErrDBConnectionFailed) - } - - m, err := migrate.NewWithSourceInstance("iofs", migrationsSource, c.connectionURL) - if err != nil { - msg := "Failed to initialize the database migration client" - log.Error(err, msg) - - _ = sendDBErrorEvent(ctx, client, controllerNamespace, msg) - - return fmt.Errorf("%w: %w", ErrDBConnectionFailed, err) - } - - defer m.Close() - - err = m.Up() - if err != nil && err.Error() == "no change" { - log.Info("The database schema is up to date") - } else if err != nil { - msg := "Failed to perform the database migration. The compliance events endpoint will not start until this " + - "is resolved." - - log.Error(err, msg) - - _ = sendDBErrorEvent(ctx, client, controllerNamespace, msg) - - return fmt.Errorf("%w: %w", ErrDBConnectionFailed, err) - } else { - // The errors don't need to be checked because we know the database migration was successful so there is a - // valid version assigned. - version, _, _ := m.Version() - // The cache gets reset after a migration in case the database changed. If the database - // was restored to an older backup, then the propagator needs to restart to clear the cache. - c.ParentPolicyToID = sync.Map{} - c.PolicyToID = sync.Map{} - - msg := fmt.Sprintf("The compliance events database schema was successfully updated to version %d", version) - log.Info(msg) - - _ = sendDBEvent(ctx, client, controllerNamespace, "Normal", "OCMComplianceEventsDB", msg) - } - - c.needsMigration = false - - return nil -} - -// ParseDBSecret will parse the input database secret and return a connection URL. If the secret contains invalid -// connection information, then ErrInvalidDBSecret is returned. -func ParseDBSecret(dbSecret *corev1.Secret, tempDirPath string) (string, error) { - var connectionURL *url.URL - var err error - - if dbSecret.Data["connectionURL"] != nil { - connectionURL, err = url.Parse(strings.TrimSpace(string(dbSecret.Data["connectionURL"]))) - if err != nil { - err := fmt.Errorf("%w: failed to parse the connectionURL value: %w", ErrInvalidDBSecret, err) - - return "", err - } - } else { - if dbSecret.Data["user"] == nil { - return "", fmt.Errorf("%w: no user value was provided", ErrInvalidDBSecret) - } - - user := string(dbSecret.Data["user"]) - - if dbSecret.Data["password"] == nil { - return "", fmt.Errorf("%w: no password value was provided", ErrInvalidDBSecret) - } - - password := string(dbSecret.Data["password"]) - - if dbSecret.Data["host"] == nil { - return "", fmt.Errorf("%w: no host value was provided", ErrInvalidDBSecret) - } - - host := string(dbSecret.Data["host"]) - - var port string - - if dbSecret.Data["port"] == nil { - log.Info("No port value was provided. Using the default 5432.") - port = "5432" - } else { - port = string(dbSecret.Data["port"]) - } - - if dbSecret.Data["dbname"] == nil { - return "", fmt.Errorf("%w: no dbname value was provided", ErrInvalidDBSecret) - } - - dbName := string(dbSecret.Data["dbname"]) - - var sslMode string - - if dbSecret.Data["sslmode"] == nil { - log.Info("No sslmode value was provided. Using the default sslmode=verify-full.") - sslMode = "verify-full" - } else { - sslMode = string(dbSecret.Data["sslmode"]) - } - - connectionURL = &url.URL{ - Scheme: "postgresql", - User: url.UserPassword(user, password), - Host: fmt.Sprintf("%s:%s", host, port), - Path: dbName, - RawQuery: "sslmode=" + url.QueryEscape(sslMode), - } - } - - if !strings.Contains(connectionURL.RawQuery, "connect_timeout=") { - if connectionURL.RawQuery != "" { - connectionURL.RawQuery += "&" - } - - // This is important or else db.Ping() takes too long if the connection is down. - connectionURL.RawQuery += "connect_timeout=5" - } - - if dbSecret.Data["ca"] != nil { - caPath := path.Join(tempDirPath, "db-ca.crt") - - err := os.WriteFile(caPath, dbSecret.Data["ca"], 0o600) - if err != nil { - return "", fmt.Errorf("failed to write the custom root CA specified in the secret: %w", err) - } - - if connectionURL.RawQuery != "" { - connectionURL.RawQuery += "&" - } - - connectionURL.RawQuery += "sslrootcert=" + url.QueryEscape(caPath) - } - - if !strings.Contains(connectionURL.RawQuery, "sslmode=verify-full") { - log.Info( - "The configured Postgres connection URL does not specify sslmode=verify-full. Please consider using a " + - "more secure connection.", - ) - } - - return connectionURL.String(), nil -} - -func sendDBEvent( - ctx context.Context, client *kubernetes.Clientset, namespace, eventType, reason, msg string, -) error { - event := &corev1.Event{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("compliance-events-api.%x", time.Now().UnixNano()), - Namespace: namespace, - }, - InvolvedObject: corev1.ObjectReference{ - Kind: "Secret", - Namespace: namespace, - Name: DBSecretName, - APIVersion: "v1", - }, - Type: eventType, - Reason: reason, - Message: msg, - Source: corev1.EventSource{ - Component: ControllerName, - }, - ReportingController: ControllerName, - } - - _, err := client.CoreV1().Events(namespace).Create(ctx, event, metav1.CreateOptions{}) - if err != nil { - log.Error(err, "Failed to send a Kubernetes warning event") - } - - return err -} - -func sendDBErrorEvent(ctx context.Context, client *kubernetes.Clientset, namespace, msg string) error { - fullMsg := msg + " See the governance-policy-propagator logs for more details." - - return sendDBEvent(ctx, client, namespace, "Warning", "OCMComplianceEventsDBError", fullMsg) -} diff --git a/controllers/complianceeventsapi/complianceeventsapi_controller_test.go b/controllers/complianceeventsapi/complianceeventsapi_controller_test.go deleted file mode 100644 index c6047e52..00000000 --- a/controllers/complianceeventsapi/complianceeventsapi_controller_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package complianceeventsapi - -import ( - "net/url" - "os" - "path" - "strings" - "testing" - - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" -) - -func TestParseDBSecret(t *testing.T) { - t.Parallel() - - const caContent = "some-ca" - - tests := []struct { - name string - secret corev1.Secret - expected string - }{ - { - name: "connectionURL-no-ssl", - secret: corev1.Secret{ - Data: map[string][]byte{ - "connectionURL": []byte("postgresql://grc:grc@localhost/db?sslmode=disable"), - }, - }, - expected: "postgresql://grc:grc@localhost/db?sslmode=disable&connect_timeout=5", - }, - { - name: "connectionURL-newline", - secret: corev1.Secret{ - Data: map[string][]byte{ - "connectionURL": []byte("postgresql://grc:grc@localhost/db?sslmode=disable\n"), - }, - }, - expected: "postgresql://grc:grc@localhost/db?sslmode=disable&connect_timeout=5", - }, - { - name: "connectionURL-ssl-ca", - secret: corev1.Secret{ - Data: map[string][]byte{ - "ca": []byte(caContent), - "connectionURL": []byte("postgresql://grc:grc@localhost/db?sslmode=verify-full"), - }, - }, - expected: "postgresql://grc:grc@localhost/db?sslmode=verify-full&connect_timeout=5", - }, - { - name: "connectionURL-custom-connect_timeout", - secret: corev1.Secret{ - Data: map[string][]byte{ - "connectionURL": []byte("postgresql://grc:grc@localhost/db?connect_timeout=30"), - }, - }, - expected: "postgresql://grc:grc@localhost/db?connect_timeout=30", - }, - { - name: "separate-with-defaults", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "password": []byte("grc"), - "host": []byte("localhost"), - "dbname": []byte("db"), - }, - }, - expected: "postgresql://grc:grc@localhost:5432/db?sslmode=verify-full&connect_timeout=5", - }, - { - name: "separate-no-defaults", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "password": []byte("grc"), - "host": []byte("localhost"), - "port": []byte("1234"), - "dbname": []byte("db"), - "sslmode": []byte("disable"), - }, - }, - expected: "postgresql://grc:grc@localhost:1234/db?sslmode=disable&connect_timeout=5", - }, - { - name: "separate-with-ca", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "password": []byte("grc"), - "host": []byte("localhost"), - "dbname": []byte("db"), - "ca": []byte(caContent), - }, - }, - expected: "postgresql://grc:grc@localhost:5432/db?sslmode=verify-full&connect_timeout=5", - }, - } - - for _, test := range tests { - test := test - - t.Run( - test.name, - func(t *testing.T) { - t.Parallel() - - g := NewWithT(t) - - tempDir, err := os.MkdirTemp("", "test-compliance-events-store") - g.Expect(err).ToNot(HaveOccurred()) - - defer os.RemoveAll(tempDir) - - connectionURL, err := ParseDBSecret(&test.secret, tempDir) - g.Expect(err).ToNot(HaveOccurred()) - - caPath := path.Join(tempDir, "db-ca.crt") - - if strings.Contains(connectionURL, "sslrootcert") { - g.Expect(connectionURL).To(Equal(test.expected + "&sslrootcert=" + url.QueryEscape(caPath))) - - readCA, err := os.ReadFile(caPath) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(string(readCA)).To(Equal(caContent)) - } else { - g.Expect(connectionURL).To(Equal(test.expected)) - _, err := os.Stat(caPath) - g.Expect(err).To(MatchError(os.ErrNotExist)) - } - }, - ) - } -} - -func TestParseDBSecretErrors(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - secret corev1.Secret - expectedSubStr string - }{ - { - name: "missing-all", - secret: corev1.Secret{ - Data: map[string][]byte{}, - }, - expectedSubStr: "no user value was provided", - }, - { - name: "missing-user", - secret: corev1.Secret{ - Data: map[string][]byte{ - "password": []byte("grc"), - "host": []byte("localhost"), - "dbname": []byte("db"), - }, - }, - expectedSubStr: "no user value was provided", - }, - { - name: "missing-password", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "host": []byte("localhost"), - "dbname": []byte("db"), - }, - }, - expectedSubStr: "no password value was provided", - }, - { - name: "missing-host", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "password": []byte("grc"), - "dbname": []byte("db"), - }, - }, - expectedSubStr: "no host value was provided", - }, - { - name: "missing-host", - secret: corev1.Secret{ - Data: map[string][]byte{ - "user": []byte("grc"), - "password": []byte("grc"), - "host": []byte("localhost"), - }, - }, - expectedSubStr: "no dbname value was provided", - }, - } - - for _, test := range tests { - test := test - - t.Run( - test.name, - func(t *testing.T) { - t.Parallel() - - g := NewWithT(t) - _, err := ParseDBSecret(&test.secret, "") - g.Expect(err).To(MatchError(ErrInvalidDBSecret)) - g.Expect(err.Error()).To(ContainSubstring(test.expectedSubStr)) - }, - ) - } -} diff --git a/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.down.sql b/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.down.sql deleted file mode 100644 index 97a4d7ea..00000000 --- a/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -BEGIN; - -DROP TABLE IF EXISTS compliance_events; -DROP TABLE IF EXISTS policies; -DROP TABLE IF EXISTS parent_policies; -DROP TABLE IF EXISTS clusters; - -COMMIT; diff --git a/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.up.sql b/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.up.sql deleted file mode 100644 index 1242a9f9..00000000 --- a/controllers/complianceeventsapi/migrations/000001_compliance_history_initial_tables.up.sql +++ /dev/null @@ -1,87 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS clusters( - id serial PRIMARY KEY, - name TEXT NOT NULL, - cluster_id TEXT UNIQUE NOT NULL, - UNIQUE (name, cluster_id) -); - -CREATE INDEX IF NOT EXISTS idx_clusters_name ON clusters (name); - -CREATE TABLE IF NOT EXISTS parent_policies( - id serial PRIMARY KEY, - name TEXT NOT NULL, - namespace TEXT NOT NULL, - categories TEXT [], - controls TEXT [], - standards TEXT [], - UNIQUE (name, namespace, categories, controls, standards) -); - --- This is required until we only support Postgres 15+ to utilize NULLS NOT DISTINCT. --- Partial indexes with 1 nullable unique field provided (e.g. A, B, C) -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null1 ON parent_policies (name, namespace, controls, standards) WHERE categories IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null2 ON parent_policies (name, namespace, categories, standards) WHERE controls IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null3 ON parent_policies (name, namespace, categories, controls) WHERE standards IS NULL; - --- Partial indexes with 2 nullable unique field provided (e.g. AB AC BC) -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null4 ON parent_policies (name, namespace, standards) WHERE categories IS NULL AND controls IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null5 ON parent_policies (name, namespace, controls) WHERE categories IS NULL AND standards IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null6 ON parent_policies (name, namespace, categories) WHERE controls IS NULL AND standards IS NULL; - --- Partial index with no nullable unique fields provided (e.g. ABC) -CREATE UNIQUE INDEX IF NOT EXISTS parent_policies_null7 ON parent_policies (name, namespace) WHERE categories IS NULL AND controls IS NULL AND standards IS NULL; - -CREATE TABLE IF NOT EXISTS policies( - id serial PRIMARY KEY, - kind TEXT NOT NULL, - api_group TEXT NOT NULL, - name TEXT NOT NULL, - namespace TEXT, - spec JSONB NOT NULL, - severity TEXT, - UNIQUE (kind, api_group, name, namespace, spec, severity) -); - --- This is required until we only support Postgres 15+ to utilize NULLS NOT DISTINCT. --- Partial indexes with 1 nullable unique field provided (e.g. A, B) -CREATE UNIQUE INDEX IF NOT EXISTS policies_null1 ON policies (kind, api_group, name, spec, severity) WHERE namespace IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS policies_null2 ON policies (kind, api_group, name, namespace, spec) WHERE severity IS NULL; - --- Partial index with no nullable unique fields provided (e.g. AB) -CREATE UNIQUE INDEX IF NOT EXISTS policies_null3 ON policies (kind, api_group, name, spec) WHERE namespace IS NULL AND severity IS NULL; - -CREATE INDEX IF NOT EXISTS idx_policies_spec ON policies (spec); - -CREATE TABLE IF NOT EXISTS compliance_events( - id serial PRIMARY KEY, - cluster_id INT NOT NULL, - policy_id INT NOT NULL, - parent_policy_id INT, - compliance TEXT NOT NULL, - message TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL, - metadata JSONB, - reported_by TEXT, - CONSTRAINT fk_policy_id - FOREIGN KEY(policy_id) - REFERENCES policies(id), - CONSTRAINT fk_parent_policy_id - FOREIGN KEY(parent_policy_id) - REFERENCES parent_policies(id), - CONSTRAINT fk_cluster_id - FOREIGN KEY(cluster_id) - REFERENCES clusters(id), - UNIQUE (cluster_id, policy_id, parent_policy_id, compliance, message, timestamp) -); - --- This is required until we only support Postgres 15+ to utilize NULLS NOT DISTINCT. --- Partial indexes with 1 nullable unique field provided (e.g. A, B) -CREATE UNIQUE INDEX IF NOT EXISTS compliance_events_null1 ON compliance_events (cluster_id, policy_id, compliance, message, timestamp) WHERE parent_policy_id IS NULL; - -CREATE INDEX IF NOT EXISTS idx_compliance_events_compliance ON compliance_events (compliance); -CREATE INDEX IF NOT EXISTS idx_compliance_events_timestamp ON compliance_events (timestamp); -CREATE INDEX IF NOT EXISTS idx_compliance_events_reported_by ON compliance_events (reported_by); - -COMMIT; diff --git a/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.down.sql b/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.down.sql deleted file mode 100644 index 555a57bd..00000000 --- a/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.down.sql +++ /dev/null @@ -1,43 +0,0 @@ -BEGIN; - -DROP EXTENSION IF EXISTS pgcrypto; - --- Drop message_hash -ALTER TABLE compliance_events DROP CONSTRAINT compliance_events_cluster_id_policy_id_parent_policy_id_com_key; -DROP INDEX compliance_events_null1; -ALTER TABLE compliance_events DROP COLUMN message_hash; -ALTER TABLE compliance_events ADD CONSTRAINT compliance_events_cluster_id_policy_id_parent_policy_id_com_key UNIQUE (cluster_id, policy_id, parent_policy_id, compliance, message, timestamp); -CREATE UNIQUE INDEX compliance_events_null1 ON compliance_events (cluster_id, policy_id, compliance, message, timestamp) WHERE parent_policy_id IS NULL; -DROP INDEX compliance_events_messages; - - --- Add back the spec column directly on the policies table -ALTER TABLE policies ADD spec JSONB; - -DO -$DO$ -DECLARE temprow RECORD; -BEGIN FOR temprow IN - SELECT "id", "spec_id" FROM policies - LOOP - UPDATE policies SET spec = (SELECT spec FROM specs WHERE id=temprow.spec_id) WHERE id = temprow.id; - END LOOP; -END; -$DO$; - -ALTER TABLE policies DROP CONSTRAINT policies_kind_api_group_name_namespace_spec_id_severity_key; -DROP INDEX policies_null1; -DROP INDEX policies_null2; -DROP INDEX policies_null3; - -ALTER TABLE policies DROP COLUMN spec_id; - -DROP TABLE specs; - -ALTER TABLE policies ADD CONSTRAINT policies_kind_api_group_name_namespace_spec_severity_key UNIQUE (kind, api_group, name, namespace, spec, severity); -CREATE UNIQUE INDEX policies_null1 ON policies (kind, api_group, name, spec, severity) WHERE namespace IS NULL; -CREATE UNIQUE INDEX policies_null2 ON policies (kind, api_group, name, namespace, spec) WHERE severity IS NULL; -CREATE UNIQUE INDEX policies_null3 ON policies (kind, api_group, name, spec) WHERE namespace IS NULL AND severity IS NULL; -CREATE INDEX idx_policies_spec ON policies (spec); - -COMMIT; diff --git a/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.up.sql b/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.up.sql deleted file mode 100644 index 2c754cc2..00000000 --- a/controllers/complianceeventsapi/migrations/000002_compliance_history_fix_indices.up.sql +++ /dev/null @@ -1,78 +0,0 @@ -BEGIN; - -CREATE EXTENSION IF NOT EXISTS pgcrypto; - --- If compliance messages are too long, the unique index gets too large and fails. This is a workaround for a unique --- constraint while still allowing for long messages. - --- Drop the soon to be invalid unique constraints. -ALTER TABLE compliance_events DROP CONSTRAINT compliance_events_cluster_id_policy_id_parent_policy_id_com_key; -DROP INDEX compliance_events_null1; - --- SHA1 hex of the message for the uniqueness constraint. -ALTER TABLE compliance_events ADD message_hash VARCHAR(40); - --- Populate the SHA1 hex -DO -$DO$ -DECLARE temprow RECORD; -BEGIN FOR temprow IN - SELECT "id", "message" FROM compliance_events - LOOP - UPDATE compliance_events SET message_hash = encode(digest(temprow.message, 'sha1'), 'hex') WHERE id = temprow.id; - END LOOP; -END; -$DO$; - -ALTER TABLE compliance_events ALTER COLUMN message_hash SET NOT NULL; - --- Set the unique constraints -ALTER TABLE compliance_events ADD CONSTRAINT compliance_events_cluster_id_policy_id_parent_policy_id_com_key UNIQUE (cluster_id, policy_id, parent_policy_id, compliance, message_hash, timestamp); - -CREATE UNIQUE INDEX compliance_events_null1 ON compliance_events (cluster_id, policy_id, compliance, message_hash, timestamp) WHERE parent_policy_id IS NULL; - --- Provide an index for equality comparisons on the message. -CREATE INDEX compliance_events_messages ON compliance_events USING HASH (message); - --- If the spec is too long, the unique index gets too large and fails. This is a workaround for a unique --- constraint while still allowing for spec uniqueness. - -CREATE TABLE specs( - id serial PRIMARY KEY, - spec JSONB NOT NULL, - EXCLUDE USING HASH (spec with =) -); - --- Drop the soon to be invalid unique constraints. -ALTER TABLE policies DROP CONSTRAINT policies_kind_api_group_name_namespace_spec_severity_key; -DROP INDEX policies_null1; -DROP INDEX policies_null2; -DROP INDEX policies_null3; -DROP INDEX idx_policies_spec; - -ALTER TABLE policies ADD spec_id INT; -ALTER TABLE policies ADD FOREIGN KEY (spec_id) REFERENCES specs(id); - --- Populate the specs table -DO -$DO$ -DECLARE temprow RECORD; -BEGIN FOR temprow IN - SELECT "id", "spec" FROM policies - LOOP - INSERT INTO specs (spec) VALUES (temprow.spec) ON CONFLICT DO NOTHING; - UPDATE policies SET spec_id = (SELECT id FROM specs WHERE spec=temprow.spec) WHERE id = temprow.id; - END LOOP; -END; -$DO$; - -ALTER TABLE policies ALTER COLUMN spec_id SET NOT NULL; - -ALTER TABLE policies DROP spec; - -ALTER TABLE policies ADD CONSTRAINT policies_kind_api_group_name_namespace_spec_id_severity_key UNIQUE (kind, api_group, name, namespace, spec_id, severity); -CREATE UNIQUE INDEX policies_null1 ON policies (kind, api_group, name, spec_id, severity) WHERE namespace IS NULL; -CREATE UNIQUE INDEX policies_null2 ON policies (kind, api_group, name, namespace, spec_id) WHERE severity IS NULL; -CREATE UNIQUE INDEX policies_null3 ON policies (kind, api_group, name, spec_id) WHERE namespace IS NULL AND severity IS NULL; - -COMMIT; diff --git a/controllers/complianceeventsapi/server.go b/controllers/complianceeventsapi/server.go deleted file mode 100644 index a0b9aef7..00000000 --- a/controllers/complianceeventsapi/server.go +++ /dev/null @@ -1,1461 +0,0 @@ -package complianceeventsapi - -import ( - "context" - "crypto/tls" - "database/sql" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - stdlog "log" - "math" - "net" - "net/http" - "net/url" - "reflect" - "slices" - "sort" - "strconv" - "strings" - "sync" - "time" - - "github.com/lib/pq" - "k8s.io/client-go/rest" -) - -// init dynamically parses the database columns of each struct type to create a mapping of user provided sort/filter -// options to the equivalent SQL column. ErrInvalidSortOption, ErrInvalidQueryArg, and validQueryArgs are also defined -// with the available sort/query options to choose from. -func init() { - tableToStruct := map[string]any{ - "clusters": Cluster{}, - "compliance_events": EventDetails{}, - "parent_policies": ParentPolicy{}, - "policies": Policy{}, - } - - tableNameToJSONName := map[string]string{ - "clusters": "cluster", - "compliance_events": "event", - "parent_policies": "parent_policy", - "policies": "policy", - } - - // ID is a special case since it's displayed at the top-level in the JSON but is actually in the compliance_events - // table. - queryOptionsToSQL = map[string]string{"id": "compliance_events.id"} - sortOptionsKeys := []string{"id"} - - for tableName, tableStruct := range tableToStruct { - structType := reflect.TypeOf(tableStruct) - for i := 0; i < structType.NumField(); i++ { - structField := structType.Field(i) - - jsonField := structField.Tag.Get("json") - if jsonField == "" || jsonField == "-" { - continue - } - - // This removes additional text in tag such as capturing `spec` from `json:"spec,omitempty"`. - jsonField = strings.SplitN(jsonField, ",", 2)[0] - - dbColumn := structField.Tag.Get("db") - if dbColumn == "" { - continue - } - - // Skip JSONB columns as sortable options - if tableName == "policies" && dbColumn == "spec" { - continue - } - - if tableName == "compliance_events" && dbColumn == "metadata" { - continue - } - - queryOption := fmt.Sprintf("%s.%s", tableNameToJSONName[tableName], jsonField) - - sortOptionsKeys = append(sortOptionsKeys, queryOption) - validQueryArgs = append(validQueryArgs, queryOption) - - queryOptionsToSQL[queryOption] = fmt.Sprintf("%s.%s", tableName, dbColumn) - } - } - - sort.Strings(sortOptionsKeys) - - ErrInvalidSortOption = fmt.Errorf( - "an invalid sort option was provided, choose from: %s", strings.Join(sortOptionsKeys, ", "), - ) - - validQueryArgs = []string{ - "direction", - "event.message_includes", - "event.message_like", - "event.timestamp_after", - "event.timestamp_before", - "include_spec", - "page", - "per_page", - "sort", - } - - validQueryArgs = append( - validQueryArgs, - // sortOptionsKeys are all filterable columns. - sortOptionsKeys..., - ) - - sort.Strings(validQueryArgs) - - ErrInvalidQueryArg = fmt.Errorf( - "an invalid query argument was provided, choose from: %s", strings.Join(validQueryArgs, ", "), - ) -} - -const ( - postgresForeignKeyViolationCode = "23503" -) - -var ( - clusterKeyCache sync.Map - queryOptionsToSQL map[string]string - validQueryArgs []string - ErrInvalidSortOption error - ErrInvalidQueryArgValue = errors.New("invalid query argument") - ErrInvalidQueryArg error - ErrUnauthorized = errors.New("not authorized") - ErrForbidden = errors.New("the request is not allowed") - // The user has no access to any managed cluster - ErrNoAccess = errors.New("the user has no access") -) - -type ComplianceAPIServer struct { - server *http.Server - addr string - cert *tls.Certificate - cfg *rest.Config -} - -func NewComplianceAPIServer(listenAddress string, cfg *rest.Config, cert *tls.Certificate) *ComplianceAPIServer { - return &ComplianceAPIServer{ - addr: listenAddress, - cert: cert, - cfg: cfg, - } -} - -type serverErrorLogWriter struct{} - -func (*serverErrorLogWriter) Write(p []byte) (int, error) { - m := string(p) - - // The OpenShift router (haproxy) seems to perform TCP checks to see if the connection is available. When it does - // this, it resets the connection when done, which causes a log message every 5 seconds, so this will filter it out. - if strings.HasPrefix(m, "http: TLS handshake error") && strings.HasSuffix(m, ": connection reset by peer\n") { - log.V(2).Info(m) - } else { - log.Info(m) - } - - return len(p), nil -} - -func newServerErrorLog() *stdlog.Logger { - return stdlog.New(&serverErrorLogWriter{}, "", 0) -} - -// Start starts the HTTP server and blocks until ctx is closed or there was an error starting the -// HTTP server. -func (s *ComplianceAPIServer) Start(ctx context.Context, serverContext *ComplianceServerCtx) error { - mux := http.NewServeMux() - - s.server = &http.Server{ - Addr: s.addr, - Handler: mux, - - // need to investigate ideal values for these - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 15 * time.Second, - ErrorLog: newServerErrorLog(), - } - - listener, err := net.Listen("tcp", s.addr) - if err != nil { - return err - } - - if s.cert != nil { - s.server.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{*s.cert}, - } - - listener = tls.NewListener(listener, s.server.TLSConfig) - } - - // register handlers here - mux.HandleFunc("/api/v1/compliance-events", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - serverContext.Lock.RLock() - defer serverContext.Lock.RUnlock() - - if serverContext.DB == nil || serverContext.DB.PingContext(r.Context()) != nil { - writeErrMsgJSON(w, "The database is unavailable", http.StatusInternalServerError) - - return - } - - switch r.Method { - case http.MethodGet: - // To verify each request independently - userConfig, err := getUserKubeConfig(s.cfg, r) - if err != nil { - if errors.Is(err, ErrUnauthorized) { - writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) - } - - return - } - getComplianceEvents(serverContext.DB, w, r, userConfig) - case http.MethodPost: - postComplianceEvent(serverContext, s.cfg, w, r) - default: - writeErrMsgJSON(w, "Method not allowed", http.StatusMethodNotAllowed) - } - }) - - mux.HandleFunc("/api/v1/compliance-events/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - serverContext.Lock.RLock() - defer serverContext.Lock.RUnlock() - - if serverContext.DB == nil || serverContext.DB.PingContext(r.Context()) != nil { - writeErrMsgJSON(w, "The database is unavailable", http.StatusInternalServerError) - - return - } - - if r.Method != http.MethodGet { - writeErrMsgJSON(w, "Method not allowed", http.StatusMethodNotAllowed) - - return - } - - // To verify each request independently - userConfig, err := getUserKubeConfig(s.cfg, r) - if err != nil { - if errors.Is(err, ErrUnauthorized) { - writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) - } - - return - } - - getSingleComplianceEvent(serverContext.DB, w, r, userConfig) - }) - - mux.HandleFunc("/api/v1/reports/compliance-events", func(w http.ResponseWriter, r *http.Request) { - // This header needs to be set universally and then overridden if a JSON error message is sent since the - // transfer is chunked. If the output is large, and multiple chunks are used, it falls back to this header. - w.Header().Set("Content-Type", "text/csv") - - // To verify each request independently - userConfig, err := getUserKubeConfig(s.cfg, r) - if err != nil { - if errors.Is(err, ErrUnauthorized) { - writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) - } - - return - } - - if r.Method != http.MethodGet { - writeErrMsgJSON(w, "Method not allowed", http.StatusMethodNotAllowed) - - return - } - - getComplianceEventsCSV(serverContext.DB, w, r, userConfig) - }) - - serveErr := make(chan error) - - go func() { - defer close(serveErr) - - err := s.server.Serve(listener) - if err != nil && err != http.ErrServerClosed { - serveErr <- err - } - }() - - select { - case <-ctx.Done(): - err := s.server.Shutdown(context.Background()) - if err != nil { - log.Error(err, "Failed to shutdown the compliance API server") - } - - return nil - case err, closed := <-serveErr: - if err != nil { - return err - } - - if closed { - return errors.New("the compliance API server unexpectedly shutdown without an error") - } - - return nil - } -} - -// splitQueryValue will parse a string and split on unescaped commas. Empty values are discarded. -func splitQueryValue(value string) []string { - values := []string{} - - var currentVal string - var previousChar rune - - for _, char := range value { - if char == ',' { - if previousChar == '\\' { - // This comma was escaped, so remove the escape character and keep the comma. Runes are used in case - // unicode characters are present in currentVal. - runeCurrentVal := []rune(currentVal) - currentVal = string(runeCurrentVal[:len(runeCurrentVal)-1]) + "," - } else { - // The comma was not escaped so we encountered a new value. - if currentVal != "" { - values = append(values, currentVal) - } - - currentVal = "" - } - } else { - // A non-special character was encountered so just append the character. - currentVal += string(char) - } - - previousChar = char - } - - if currentVal != "" { - values = append(values, currentVal) - } - - return values -} - -// parseQueryArgs will parse the HTTP request's query arguments and convert them to a usable format for constructing -// the SQL query. All defaults are set and any invalid query arguments result in an error being returned. -func parseQueryArgs(ctx context.Context, queryArgs url.Values, db *sql.DB, - userConfig *rest.Config, isCSV bool, -) (*queryOptions, error) { - parsed := &queryOptions{ - Direction: "desc", - Page: 1, - PerPage: 20, - Sort: []string{"compliance_events.timestamp"}, - ArrayFilters: map[string][]string{}, - Filters: map[string][]string{}, - NullFilters: []string{}, - } - - // Case return CSV file, default PerPage is 0. Unlimited - if isCSV { - parsed.PerPage = 0 - } - - for arg := range queryArgs { - valid := false - - for _, validQueryArg := range validQueryArgs { - if arg == validQueryArg { - valid = true - - break - } - } - - if !valid { - return nil, ErrInvalidQueryArg - } - - sqlName, hasSQLName := queryOptionsToSQL[arg] - - value := queryArgs.Get(arg) - if value == "" && arg != "include_spec" { - // Only support null filters if it's a SQL column - if !hasSQLName { - return nil, fmt.Errorf("%w: %s must have a value", ErrInvalidQueryArgValue, arg) - } - - parsed.NullFilters = append(parsed.NullFilters, sqlName) - - continue - } - - switch arg { - case "direction": - if value == "desc" { - parsed.Direction = "DESC" - } else if value == "asc" { - parsed.Direction = "ASC" - } else { - return nil, fmt.Errorf("%w: direction must be one of: asc, desc", ErrInvalidQueryArg) - } - case "include_spec": - if value != "" { - return nil, fmt.Errorf("%w: include_spec is a flag and does not accept a value", ErrInvalidQueryArg) - } - - parsed.IncludeSpec = true - case "page": - var err error - - parsed.Page, err = strconv.ParseUint(value, 10, 64) - if err != nil || parsed.Page == 0 { - return nil, fmt.Errorf("%w: page must be a positive integer", ErrInvalidQueryArg) - } - case "per_page": - var err error - - parsed.PerPage, err = strconv.ParseUint(value, 10, 64) - if err != nil || parsed.PerPage == 0 || parsed.PerPage > 100 { - return nil, fmt.Errorf("%w: per_page must be a value between 1 and 100", ErrInvalidQueryArg) - } - case "sort": - sortArgs := splitQueryValue(value) - - sortSQL := []string{} - - for _, sortArg := range sortArgs { - sortOption, ok := queryOptionsToSQL[sortArg] - if !ok { - return nil, ErrInvalidSortOption - } - - sortSQL = append(sortSQL, sortOption) - } - - parsed.Sort = sortSQL - case "parent_policy.categories", "parent_policy.controls", "parent_policy.standards": - parsed.ArrayFilters[sqlName] = splitQueryValue(value) - case "event.message_includes": - // Escape the SQL LIKE operators because we aren't exposing that functionality. - escapedVal := strings.ReplaceAll(value, "%", `\%`) - escapedVal = strings.ReplaceAll(escapedVal, "_", `\_`) - // Add wildcards at the beginning and end of the search keyword for substring matching. - parsed.MessageIncludes = "%" + escapedVal + "%" - case "event.message_like": - parsed.MessageLike = value - case "event.timestamp_before": - var err error - - parsed.TimestampBefore, err = time.Parse(time.RFC3339, value) - if err != nil { - return nil, fmt.Errorf( - "%w: event.timestamp_before must be in the format of RFC 3339", ErrInvalidQueryArgValue, - ) - } - case "event.timestamp_after": - var err error - - parsed.TimestampAfter, err = time.Parse(time.RFC3339, value) - if err != nil { - return nil, fmt.Errorf( - "%w: event.timestamp_after must be in the format of RFC 3339", ErrInvalidQueryArgValue, - ) - } - default: - // Standard string filtering - parsed.Filters[sqlName] = splitQueryValue(value) - } - } - - parsed, err := setAuthorizedClusters(ctx, db, parsed, userConfig) - if err != nil { - // ErrNoAccess needs queryOptions - return parsed, err - } - - return parsed, nil -} - -// setAuthorizedClusters verifies that if a cluster filter is provided, -// the user has access to this filter. If no cluster filter is provided, -// it sets the cluster filter to all managed clusters the user has access to. -// If the user has no access, then ErrNoAccess is returned. -func setAuthorizedClusters(ctx context.Context, db *sql.DB, parsed *queryOptions, - userConfig *rest.Config, -) (*queryOptions, error) { - unAuthorizedClusters := []string{} - - // Get all managedCluster rules - allRules, err := getManagedClusterRules(userConfig, nil) - if err != nil { - return parsed, err - } - - if slices.Contains(allRules["*"], "get") || slices.Contains(allRules["*"], "*") { - return parsed, nil - } - - clusterIDs := parsed.Filters["clusters.cluster_id"] - // Temporarily reset clusters.cluster_id and repopulate with all known cluster IDs - parsed.Filters["clusters.cluster_id"] = []string{} - - // Convert id to name - for _, id := range clusterIDs { - clusterName, err := getClusterNameFromID(ctx, db, id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - // Filter out invalid cluster IDs from the query - continue - } - - log.Error(err, "Failed to get cluster name from cluster ID", getPqErrKeyVals(err, "ID", id)...) - - return parsed, err - } - - if !getAccessByClusterName(allRules, clusterName) { - unAuthorizedClusters = append(unAuthorizedClusters, id) - } else { - parsed.Filters["clusters.cluster_id"] = append(parsed.Filters["clusters.cluster_id"], id) - } - } - - parsedClusterNames := parsed.Filters["clusters.name"] - for _, clusterName := range parsedClusterNames { - if !getAccessByClusterName(allRules, clusterName) { - unAuthorizedClusters = append(unAuthorizedClusters, clusterName) - } - } - - // There is no cluster.cluster_id or cluster.name query argument. - // In other words, the user requests all they have access to. - if len(clusterIDs) == 0 && len(parsedClusterNames) == 0 { - for mcName := range allRules { - // Add the cluster to the filter if the user has get authentication. Note that if the user has get access - // on all managed clusters, that gets handled at the beginning of the function. - if getAccessByClusterName(allRules, mcName) { - parsed.Filters["clusters.name"] = append(parsed.Filters["clusters.name"], mcName) - } - } - } - - if len(unAuthorizedClusters) > 0 { - return parsed, fmt.Errorf("%w: the following cluster filters are not authorized: %s", - ErrForbidden, strings.Join(unAuthorizedClusters, ", ")) - } - - if len(parsed.Filters["clusters.name"]) == 0 && len(parsed.Filters["clusters.cluster_id"]) == 0 { - return parsed, ErrNoAccess - } - - return parsed, nil -} - -// generateGetComplianceEventsQuery will return a SELECT query with results ready to be parsed by -// scanIntoComplianceEvent. The caller is responsible for adding filters to the query. -func generateGetComplianceEventsQuery(includeSpec bool) string { - query := `SELECT %s -FROM - compliance_events - LEFT JOIN clusters ON compliance_events.cluster_id = clusters.id - LEFT JOIN parent_policies ON compliance_events.parent_policy_id = parent_policies.id - LEFT JOIN policies ON compliance_events.policy_id = policies.id` - - if includeSpec { - query += "\n LEFT JOIN specs ON policies.spec_id=specs.id" - } - - return fmt.Sprintf(query, strings.Join(generateSelectedArgs(includeSpec), ", ")) -} - -func generateSelectedArgs(includeSpec bool) []string { - selectArgs := []string{ - "compliance_events.id", - "compliance_events.compliance", - "compliance_events.message", - "compliance_events.metadata", - "compliance_events.reported_by", - "compliance_events.timestamp", - "clusters.cluster_id", - "clusters.name", - "parent_policies.id", - "parent_policies.name", - "parent_policies.namespace", - "parent_policies.categories", - "parent_policies.controls", - "parent_policies.standards", - "policies.id", - "policies.api_group", - "policies.kind", - "policies.name", - "policies.namespace", - "policies.severity", - } - - if includeSpec { - selectArgs = append(selectArgs, "specs.spec") - } - - return selectArgs -} - -// generate Headers for CSV. "." replace by "_" -// Example: parent_policies.namespace -> parent_policies_namespace -func getCsvHeader(includeSpec bool) []string { - localSelectArgs := generateSelectedArgs(includeSpec) - - for i, arg := range localSelectArgs { - if arg == "specs.spec" { - localSelectArgs[i] = "policies_spec" - } else { - localSelectArgs[i] = strings.ReplaceAll(arg, ".", "_") - } - } - - return localSelectArgs -} - -type Scannable interface { - Scan(dest ...any) error -} - -// scanIntoComplianceEvent will scan the row result from the SELECT query generated by generateGetComplianceEventsQuery -// into a ComplianceEvent object. -func scanIntoComplianceEvent(rows Scannable, includeSpec bool) (*ComplianceEvent, error) { - ce := ComplianceEvent{ - Cluster: Cluster{}, - Event: EventDetails{}, - ParentPolicy: nil, - Policy: Policy{}, - } - - ppID := sql.NullInt32{} - ppName := sql.NullString{} - ppNamespace := sql.NullString{} - ppCategories := pq.StringArray{} - ppControls := pq.StringArray{} - ppStandards := pq.StringArray{} - - scanArgs := []any{ - &ce.EventID, - &ce.Event.Compliance, - &ce.Event.Message, - &ce.Event.Metadata, - &ce.Event.ReportedBy, - &ce.Event.Timestamp, - &ce.Cluster.ClusterID, - &ce.Cluster.Name, - &ppID, - &ppName, - &ppNamespace, - &ppCategories, - &ppControls, - &ppStandards, - &ce.Policy.KeyID, - &ce.Policy.APIGroup, - &ce.Policy.Kind, - &ce.Policy.Name, - &ce.Policy.Namespace, - &ce.Policy.Severity, - } - - if includeSpec { - scanArgs = append(scanArgs, &ce.Policy.Spec) - } - - err := rows.Scan(scanArgs...) - if err != nil { - return nil, err - } - - // The parent policy is optional but when it's set, the name is guaranteed to be set. - if ppName.String != "" { - ce.ParentPolicy = &ParentPolicy{ - KeyID: ppID.Int32, - Name: ppName.String, - Namespace: ppNamespace.String, - Categories: ppCategories, - Controls: ppControls, - Standards: ppStandards, - } - } - - return &ce, nil -} - -// getSingleComplianceEvent handles the GET API endpoint for a single compliance event by ID. -func getSingleComplianceEvent(db *sql.DB, w http.ResponseWriter, - r *http.Request, config *rest.Config, -) { - eventIDStr := strings.TrimPrefix(r.URL.Path, "/api/v1/compliance-events/") - - eventID, err := strconv.ParseUint(eventIDStr, 10, 64) - if err != nil { - writeErrMsgJSON(w, "The provided compliance event ID is invalid", http.StatusBadRequest) - - return - } - - query := fmt.Sprintf("%s\nWHERE compliance_events.id = $1;", generateGetComplianceEventsQuery(true)) - - row := db.QueryRowContext(r.Context(), query, eventID) - if row.Err() != nil { - log.Error(row.Err(), "Failed to query for the compliance event", getPqErrKeyVals(err, "eventID", eventID)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - complianceEvent, err := scanIntoComplianceEvent(row, true) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - writeErrMsgJSON(w, "The requested compliance event was not found", http.StatusNotFound) - - return - } - - log.Error(err, "Failed to unmarshal the database results", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - // Check auth for managedCluster GET verb - isAllowed, err := canGetManagedCluster(config, complianceEvent.Cluster.Name) - if err != nil { - log.Error(err, `Failed to get the "get" authorization for the cluster`, - "cluster", complianceEvent.Cluster.Name) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - if !isAllowed { - writeErrMsgJSON(w, "Forbidden", http.StatusForbidden) - - return - } - - jsonResp, err := json.Marshal(complianceEvent) - if err != nil { - log.Error(err, "Failed marshal the compliance event", "eventID", eventID) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - if _, err = w.Write(jsonResp); err != nil { - log.Error(err, "Error writing success response") - } -} - -// getPqErrKeyVals is a helper to add additional database error details to a log message. additionalKeyVals is provided -// as a convenience so that the keys don't need to be explicitly set to interface{} types when using the -// `getPqErrKeyVals(err, "key1", "val1")...“ syntax. -func getPqErrKeyVals(err error, additionalKeyVals ...interface{}) []interface{} { - var pqErr *pq.Error - - if errors.As(err, &pqErr) { - return append( - []interface{}{"dbMessage", pqErr.Message, "dbDetail", pqErr.Detail, "dbCode", pqErr.Code}, - additionalKeyVals..., - ) - } - - return additionalKeyVals -} - -func getClusterNameFromID(ctx context.Context, db *sql.DB, clusterID string) (name string, err error) { - err = db.QueryRowContext(ctx, - `SELECT name FROM clusters WHERE cluster_id = $1`, clusterID, - ).Scan(&name) - if err != nil { - return "", err - } - - return name, nil -} - -// getWhereClause will convert the input queryOptions to a WHERE statement and return the filter values for a prepared -// statement. -func getWhereClause(options *queryOptions) (string, []any) { - filterSQL := []string{} - filterValues := []any{} - - for sqlColumn, values := range options.Filters { - if len(values) == 0 { - continue - } - - for i, value := range values { - filterValues = append(filterValues, value) - // For example: compliance_events.name=$1 - filter := fmt.Sprintf("%s=$%d", sqlColumn, len(filterValues)) - if i == 0 { - filterSQL = append(filterSQL, "("+filter) - } else { - filterSQL[len(filterSQL)-1] += " OR " + filter - } - } - - filterSQL[len(filterSQL)-1] += ")" - } - - for sqlColumn, values := range options.ArrayFilters { - if len(values) == 0 { - continue - } - - for i, value := range values { - filterValues = append(filterValues, value) - - // For example: $1=ANY(parent_policies.categories) - filter := fmt.Sprintf("$%d=ANY(%s)", len(filterValues), sqlColumn) - if i == 0 { - filterSQL = append(filterSQL, "("+filter) - } else { - filterSQL[len(filterSQL)-1] += " OR " + filter - } - } - - filterSQL[len(filterSQL)-1] += ")" - } - - for _, sqlColumn := range options.NullFilters { - filterSQL = append(filterSQL, fmt.Sprintf("%s IS NULL", sqlColumn)) - } - - if options.MessageIncludes != "" { - filterValues = append(filterValues, options.MessageIncludes) - - filterSQL = append(filterSQL, fmt.Sprintf("compliance_events.message LIKE $%d", len(filterValues))) - } - - if options.MessageLike != "" { - filterValues = append(filterValues, options.MessageLike) - - filterSQL = append(filterSQL, fmt.Sprintf("compliance_events.message LIKE $%d", len(filterValues))) - } - - if !options.TimestampAfter.IsZero() { - filterValues = append(filterValues, options.TimestampAfter) - - filterSQL = append(filterSQL, fmt.Sprintf("compliance_events.timestamp > $%d", len(filterValues))) - } - - if !options.TimestampBefore.IsZero() { - filterValues = append(filterValues, options.TimestampBefore) - - filterSQL = append(filterSQL, fmt.Sprintf("compliance_events.timestamp < $%d", len(filterValues))) - } - - var whereClause string - - if len(filterSQL) > 0 { - // For example: - // WHERE (policy.name=$1) AND ($2=ANY(parent_policies.categories) OR $3=ANY(parent_policies.categories)) - whereClause = "\nWHERE " + strings.Join(filterSQL, " AND ") - } - - return whereClause, filterValues -} - -// getComplianceEvents handles the list API endpoint for compliance events. -func getComplianceEvents(db *sql.DB, w http.ResponseWriter, - r *http.Request, userConfig *rest.Config, -) { - queryArgs, err := parseQueryArgs(r.Context(), r.URL.Query(), db, userConfig, false) - if err != nil { - if errors.Is(err, ErrForbidden) { - writeErrMsgJSON(w, err.Error(), http.StatusForbidden) - - return - } - - if errors.Is(err, ErrNoAccess) { - response := ListResponse{ - Data: []ComplianceEvent{}, - Metadata: metadata{ - Page: queryArgs.Page, - Pages: 0, - PerPage: queryArgs.PerPage, - Total: 0, - }, - } - - jsonResp, err := json.Marshal(response) - if err != nil { - log.Error(err, "Failed to marshal an empty response") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - if _, err = w.Write(jsonResp); err != nil { - log.Error(err, "Error writing empty response") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - } - - return - } - - if errors.Is(err, ErrInvalidQueryArg) || errors.Is(err, ErrInvalidQueryArgValue) || - errors.Is(err, ErrInvalidSortOption) { - writeErrMsgJSON(w, err.Error(), http.StatusBadRequest) - - return - } - - log.Error(err, "parsing the query arguments unexpectedly failed") - - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - // Note that the where clause could be an empty string if not filters were passed in the query arguments. - whereClause, filterValues := getWhereClause(queryArgs) - - query := getComplianceEventsQuery(whereClause, queryArgs) - - rows, err := db.QueryContext(r.Context(), query, filterValues...) - if err == nil { - err = rows.Err() - } - - if err != nil { - log.Error(err, "Failed to query for compliance events", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - defer rows.Close() - - complianceEvents := make([]ComplianceEvent, 0, queryArgs.PerPage) - - for rows.Next() { - ce, err := scanIntoComplianceEvent(rows, queryArgs.IncludeSpec) - if err != nil { - log.Error(err, "Failed to unmarshal the database results") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - complianceEvents = append(complianceEvents, *ce) - } - - countQuery := `SELECT COUNT(*) FROM compliance_events -LEFT JOIN clusters ON compliance_events.cluster_id = clusters.id -LEFT JOIN parent_policies ON compliance_events.parent_policy_id = parent_policies.id -LEFT JOIN policies ON compliance_events.policy_id = policies.id` + whereClause // #nosec G202 - - row := db.QueryRowContext(r.Context(), countQuery, filterValues...) - - var total uint64 - - if err := row.Scan(&total); err != nil { - log.Error(err, "Failed to get the count of compliance events", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - pages := math.Ceil(float64(total) / float64(queryArgs.PerPage)) - - response := ListResponse{ - Data: complianceEvents, - Metadata: metadata{ - Page: queryArgs.Page, - Pages: uint64(pages), - PerPage: queryArgs.PerPage, - Total: total, - }, - } - - jsonResp, err := json.Marshal(response) - if err != nil { - log.Error(err, "Failed to marshal the response") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - if _, err = w.Write(jsonResp); err != nil { - log.Error(err, "Error writing success response") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } -} - -// postComplianceEvent assumes you have a read lock already attained. -func postComplianceEvent(serverContext *ComplianceServerCtx, cfg *rest.Config, w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - log.Error(err, "error reading request body") - writeErrMsgJSON(w, "Could not read request body", http.StatusBadRequest) - - return - } - - reqEvent := &ComplianceEvent{} - - if err := json.Unmarshal(body, reqEvent); err != nil { - writeErrMsgJSON(w, "Incorrectly formatted request body, must be valid JSON", http.StatusBadRequest) - - return - } - - if err := reqEvent.Validate(r.Context(), serverContext); err != nil { - writeErrMsgJSON(w, err.Error(), http.StatusBadRequest) - - return - } - - allowed, err := canRecordComplianceEvent(cfg, reqEvent.Cluster.Name, r) - if err != nil { - if errors.Is(err, ErrUnauthorized) { - writeErrMsgJSON(w, "Unauthorized", http.StatusUnauthorized) - - return - } - - log.Error(err, "error determining if the user is authorized for recording compliance events") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - if !allowed { - // Logging is handled by canRecordComplianceEvent - writeErrMsgJSON(w, "Forbidden", http.StatusForbidden) - - return - } - - clusterFK, err := GetClusterForeignKey(r.Context(), serverContext.DB, reqEvent.Cluster) - if err != nil { - log.Error(err, "error getting cluster foreign key", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - reqEvent.Event.ClusterID = clusterFK - - if reqEvent.ParentPolicy != nil { - pfk, err := getParentPolicyForeignKey(r.Context(), serverContext, *reqEvent.ParentPolicy) - if err != nil { - log.Error(err, "error getting parent policy foreign key", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - reqEvent.Event.ParentPolicyID = &pfk - } - - policyFK, err := getPolicyForeignKey(r.Context(), serverContext, reqEvent.Policy) - if err != nil { - log.Error(err, "error getting policy foreign key", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - reqEvent.Event.PolicyID = policyFK - - err = reqEvent.Create(r.Context(), serverContext.DB) - if err != nil { - if errors.Is(err, errDuplicateComplianceEvent) { - writeErrMsgJSON(w, "The compliance event already exists", http.StatusConflict) - - return - } - - var pqErr *pq.Error - - if errors.As(err, &pqErr) && pqErr.Code == postgresForeignKeyViolationCode { - // This can only happen if the cache is out of date due to data loss in the database because if the - // database ID is provided, it is validated against the database. - log.Info( - "Encountered a foreign key violation. Assuming the database lost data, so the cache is "+ - "being cleared", - "message", pqErr.Message, - "detail", pqErr.Detail, - ) - - // Temporarily upgrade the lock to a write lock - serverContext.Lock.RUnlock() - serverContext.Lock.Lock() - serverContext.ParentPolicyToID = sync.Map{} - serverContext.PolicyToID = sync.Map{} - clusterKeyCache = sync.Map{} - serverContext.Lock.Unlock() - serverContext.Lock.RLock() - } else { - log.Error(err, "error inserting compliance event", getPqErrKeyVals(err)...) - } - - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - // remove the spec so it's not returned in the JSON. - reqEvent.Policy.Spec = nil - - resp, err := json.Marshal(reqEvent) - if err != nil { - log.Error(err, "error marshaling reqEvent for the response") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - w.WriteHeader(http.StatusCreated) - - if _, err = w.Write(resp); err != nil { - log.Error(err, "error writing success response") - } -} - -func getComplianceEventsQuery(whereClause string, queryArgs *queryOptions) string { - // Getting CSV without the page argument - // Query should fetch all rows (unlimited) - if queryArgs.PerPage == 0 { - return fmt.Sprintf(`%s%s - ORDER BY %s %s;`, - generateGetComplianceEventsQuery(queryArgs.IncludeSpec), - whereClause, - strings.Join(queryArgs.Sort, ", "), - queryArgs.Direction, - ) - } - // Example query - // SELECT compliance_events.id, compliance_events.compliance, ... - // FROM compliance_events - // LEFT JOIN clusters ON compliance_events.cluster_id = clusters.id - // LEFT JOIN parent_policies ON compliance_events.parent_policy_id = parent_policies.id - // LEFT JOIN policies ON compliance_events.policy_id = policies.id - // LEFT JOIN specs ON policies.spec_id=specs.id - // WHERE (policies.name=$1 OR policies.name=$2) AND (policies.kind=$3) - // ORDER BY compliance_events.timestamp desc - // LIMIT 20 - // OFFSET 0 ROWS; - return fmt.Sprintf(`%s%s - ORDER BY %s %s - LIMIT %d - OFFSET %d ROWS;`, - generateGetComplianceEventsQuery(queryArgs.IncludeSpec), - whereClause, - strings.Join(queryArgs.Sort, ", "), - queryArgs.Direction, - queryArgs.PerPage, - (queryArgs.Page-1)*queryArgs.PerPage, - ) -} - -func getComplianceEventsCSV(db *sql.DB, w http.ResponseWriter, r *http.Request, - userConfig *rest.Config, -) { - var writer *csv.Writer - - queryArgs, queryArgsErr := parseQueryArgs(r.Context(), r.URL.Query(), db, userConfig, true) - if queryArgs != nil { - headers := getCsvHeader(queryArgs.IncludeSpec) - - writer = csv.NewWriter(w) - - err := writer.Write(headers) - if err != nil { - log.Error(err, "Failed to write csv header") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("Content-Disposition", "attachment; filename=reports.csv") - } - - if queryArgsErr != nil { - if errors.Is(queryArgsErr, ErrForbidden) { - writeErrMsgJSON(w, queryArgsErr.Error(), http.StatusForbidden) - - return - } - - if errors.Is(queryArgsErr, ErrNoAccess) { - writer.Flush() - - return - } - - if errors.Is(queryArgsErr, ErrInvalidQueryArg) || errors.Is(queryArgsErr, ErrInvalidQueryArgValue) || - errors.Is(queryArgsErr, ErrInvalidSortOption) { - writeErrMsgJSON(w, queryArgsErr.Error(), http.StatusBadRequest) - - return - } - - log.Error(queryArgsErr, "parsing the query arguments unexpectedly failed") - - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - // Note that the where clause could be an empty string if no filters were passed in the query arguments. - whereClause, filterValues := getWhereClause(queryArgs) - - query := getComplianceEventsQuery(whereClause, queryArgs) - - rows, err := db.QueryContext(r.Context(), query, filterValues...) - if err == nil { - err = rows.Err() - } - - if err != nil { - log.Error(err, "Failed to query for compliance events", getPqErrKeyVals(err)...) - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - defer rows.Close() - - numLines := 0 - - for rows.Next() { - ce, err := scanIntoComplianceEvent(rows, queryArgs.IncludeSpec) - if err != nil { - log.Error(err, "Failed to unmarshal the database results") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - stringValues := convertToCsvLine(ce, queryArgs.IncludeSpec) - - err = writer.Write(stringValues) - if err != nil { - log.Error(err, "Failed to write csv list") - writeErrMsgJSON(w, "Internal Error", http.StatusInternalServerError) - - return - } - - numLines++ - - // Generate a chunk every 100 lines - if numLines%100 == 0 { - w.(http.Flusher).Flush() - } - } - - writer.Flush() -} - -func convertToCsvLine(ce *ComplianceEvent, includeSpec bool) []string { - nilString := "" - - if ce.ParentPolicy == nil { - ce.ParentPolicy = &ParentPolicy{ - KeyID: 0, - Name: "", - Namespace: "", - Categories: nil, - Controls: nil, - Standards: nil, - } - } - - if ce.Event.ReportedBy == nil { - ce.Event.ReportedBy = &nilString - } - - if ce.Policy.Severity == nil { - ce.Policy.Severity = &nilString - } - - if ce.Policy.Namespace == nil { - ce.Policy.Namespace = &nilString - } - - values := []string{ - convertToString(ce.EventID), - convertToString(ce.Event.Compliance), - convertToString(ce.Event.Message), - convertToString(ce.Event.Metadata), - convertToString(*ce.Event.ReportedBy), - convertToString(ce.Event.Timestamp), - convertToString(ce.Cluster.ClusterID), - convertToString(ce.Cluster.Name), - convertToString(ce.ParentPolicy.KeyID), - convertToString(ce.ParentPolicy.Name), - convertToString(ce.ParentPolicy.Namespace), - convertToString(ce.ParentPolicy.Categories), - convertToString(ce.ParentPolicy.Controls), - convertToString(ce.ParentPolicy.Standards), - convertToString(ce.Policy.KeyID), - convertToString(ce.Policy.APIGroup), - convertToString(ce.Policy.Kind), - convertToString(ce.Policy.Name), - convertToString(*ce.Policy.Namespace), - convertToString(*ce.Policy.Severity), - } - - if includeSpec { - values = append(values, convertToString(ce.Policy.Spec)) - } - - return values -} - -func convertToString(v interface{}) string { - switch vv := v.(type) { - case *string: - if vv == nil { - return "" - } - - return *vv - case string: - return vv - case int32: - // All int32 related id - if int(vv) == 0 { - return "" - } - - return strconv.Itoa(int(vv)) - case time.Time: - return vv.String() - case pq.StringArray: - // nil will be [] - return strings.Join(vv, ", ") - case bool: - return strconv.FormatBool(vv) - case JSONMap: - if vv == nil { - return "" - } - - jsonByte, err := json.MarshalIndent(vv, "", " ") - if err != nil { - return "" - } - - return string(jsonByte) - default: - // case nil: - return fmt.Sprintf("%v", vv) - } -} - -// GetClusterForeignKey will return the database ID based on the cluster.ClusterID. -func GetClusterForeignKey(ctx context.Context, db *sql.DB, cluster Cluster) (int32, error) { - // Check cache - key, ok := clusterKeyCache.Load(cluster.ClusterID) - if ok { - return key.(int32), nil - } - - err := cluster.GetOrCreate(ctx, db) - if err != nil { - return 0, err - } - - clusterKeyCache.Store(cluster.ClusterID, cluster.KeyID) - - return cluster.KeyID, nil -} - -func getParentPolicyForeignKey( - ctx context.Context, complianceServerCtx *ComplianceServerCtx, parent ParentPolicy, -) (int32, error) { - if parent.KeyID != 0 { - return parent.KeyID, nil - } - - // Check cache - parKey := parent.Key() - - key, ok := complianceServerCtx.ParentPolicyToID.Load(parKey) - if ok { - return key.(int32), nil - } - - err := parent.GetOrCreate(ctx, complianceServerCtx.DB) - if err != nil { - return 0, err - } - - complianceServerCtx.ParentPolicyToID.Store(parKey, parent.KeyID) - - return parent.KeyID, nil -} - -func getPolicyForeignKey(ctx context.Context, complianceServerCtx *ComplianceServerCtx, pol Policy) (int32, error) { - if pol.KeyID != 0 { - return pol.KeyID, nil - } - - // Check cache - polKey := pol.Key() - - key, ok := complianceServerCtx.PolicyToID.Load(polKey) - if ok { - return key.(int32), nil - } - - err := pol.GetOrCreate(ctx, complianceServerCtx.DB) - if err != nil { - return 0, err - } - - complianceServerCtx.PolicyToID.Store(polKey, pol.KeyID) - - return pol.KeyID, nil -} - -type errorMessage struct { - Message string `json:"message"` -} - -// writeErrMsgJSON wraps the given message in JSON like `{"message": <>}` and -// writes the response, setting the header to the given code. Since this message -// will be read by the user, take care not to leak any sensitive details that -// might be in the error message. -func writeErrMsgJSON(w http.ResponseWriter, message string, code int) { - msg := errorMessage{Message: message} - - resp, err := json.Marshal(msg) - if err != nil { - log.Error(err, "error marshaling error message", "message", message) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - - if _, err := w.Write(resp); err != nil { - log.Error(err, "error writing error message") - } -} diff --git a/controllers/complianceeventsapi/server_test.go b/controllers/complianceeventsapi/server_test.go deleted file mode 100644 index b57a1b69..00000000 --- a/controllers/complianceeventsapi/server_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package complianceeventsapi - -import ( - "fmt" - "testing" - "time" - - . "github.com/onsi/gomega" -) - -func TestSplitQueryValue(t *testing.T) { - t.Parallel() - - tests := []struct { - queryVal string - expected []string - }{ - {"cluster1", []string{"cluster1"}}, - {"cluster1,", []string{"cluster1"}}, - {",cluster1", []string{"cluster1"}}, - {"cluster1,cluster2", []string{"cluster1", "cluster2"}}, - {`cluster\,monkey,not-monkey`, []string{`cluster,monkey`, "not-monkey"}}, - } - - for _, test := range tests { - test := test - - t.Run( - fmt.Sprintf("?cluster.name=%s", test.queryVal), - func(t *testing.T) { - t.Parallel() - - g := NewWithT(t) - g.Expect(splitQueryValue(test.queryVal)).To(Equal(test.expected)) - }, - ) - } -} - -func TestConvertToCsvLine(t *testing.T) { - t.Parallel() - - theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC) - - reportBy := "cat1" - - ce := ComplianceEvent{ - EventID: 1, - Event: EventDetails{ - Compliance: "cp1", - Message: "event1 message", - Metadata: nil, - ReportedBy: &reportBy, - Timestamp: theTime, - }, - Cluster: Cluster{ - ClusterID: "1111", - Name: "cluster1", - }, - Policy: Policy{ - KeyID: 0, - Kind: "", - APIGroup: "v1", - Name: "", - Spec: map[string]interface{}{ - "name": "hi", - "namespace": "cat-1", - }, - }, - } - - values := convertToCsvLine(&ce, true) - - g := NewWithT(t) - g.Expect(values).Should(HaveLen(21)) - // Should follow this order - // "compliance_events_id", - // "compliance_events_compliance", - // "compliance_events_message", - // "compliance_events_metadata", - // "compliance_events_reported_by", - // "compliance_events_timestamp", - // "clusters_cluster_id", - // "clusters_name", - // "parent_policies_id", - // "parent_policies_name", - // "parent_policies_namespace", - // "parent_policies_categories", - // "parent_policies_controls", - // "parent_policies_standards", - // "policies_id", - // "policies_api_group", - // "policies_kind", - // "policies_name", - // "policies_namespace", - // "policies_severity", - // "policies_spec", - g.Expect(values).Should(Equal([]string{ - "1", "cp1", "event1 message", - "", "cat1", "2021-08-15 14:30:45.0000001 +0000 UTC", - "1111", "cluster1", "", "", "", "", "", "", "", "v1", "", "", "", "", - "{\n \"name\": \"hi\",\n \"namespace\": \"cat-1\"\n}", - })) - - // Test includeSpec = false - values = convertToCsvLine(&ce, false) - g.Expect(values).Should(HaveLen(20), "Test Some fields set") - - parentPolicy := &ParentPolicy{ - KeyID: 11, - Name: "parent-my-name", - Namespace: "ns-pp", - Categories: []string{"cate-1", "cate-2"}, - Controls: []string{"control-1", "control-2"}, - Standards: []string{"stand-1", "stand-2"}, - } - - // Test All fields set - ce = ComplianceEvent{ - EventID: 1, - ParentPolicy: parentPolicy, - Event: EventDetails{ - Compliance: "cp1", - Message: "event1 message", - Metadata: JSONMap{ - "pet": "cat1", - "flower": []string{"rose", "sunflower"}, - "number": 1, - }, - ReportedBy: &reportBy, - Timestamp: theTime, - }, - Cluster: Cluster{ - ClusterID: "22", - Name: "cluster1", - }, - Policy: Policy{ - KeyID: 0, - Kind: "configuration", - APIGroup: "v1", - Name: "policy-name", - Spec: JSONMap{ - "name": "hi", - "namespace": "cat-1", - }, - }, - } - - values = convertToCsvLine(&ce, true) - g.Expect(values).Should(Equal([]string{ - "1", "cp1", "event1 message", - "{\n \"flower\": [\n \"rose\",\n \"sunflower\"\n ],\n \"number\": 1,\n \"pet\": \"cat1\"\n}", - "cat1", "2021-08-15 14:30:45.0000001 +0000 UTC", "22", "cluster1", - "11", "parent-my-name", "ns-pp", "cate-1, cate-2", - "control-1, control-2", "stand-1, stand-2", "", - "v1", "configuration", "policy-name", "", "", - "{\n \"name\": \"hi\",\n \"namespace\": \"cat-1\"\n}", - }), "Test All fields set") -} - -func TestGetCsvHeader(t *testing.T) { - g := NewWithT(t) - - result := getCsvHeader(true) - g.Expect(result).Should(HaveLen(21)) - g.Expect(result).Should(Equal([]string{ - "compliance_events_id", - "compliance_events_compliance", - "compliance_events_message", "compliance_events_metadata", - "compliance_events_reported_by", "compliance_events_timestamp", "clusters_cluster_id", - "clusters_name", "parent_policies_id", "parent_policies_name", - "parent_policies_namespace", "parent_policies_categories", "parent_policies_controls", - "parent_policies_standards", "policies_id", "policies_api_group", "policies_kind", "policies_name", - "policies_namespace", "policies_severity", "policies_spec", - })) - - result = getCsvHeader(false) - g.Expect(result).Should(HaveLen(20)) -} diff --git a/controllers/complianceeventsapi/types.go b/controllers/complianceeventsapi/types.go deleted file mode 100644 index 3010b560..00000000 --- a/controllers/complianceeventsapi/types.go +++ /dev/null @@ -1,644 +0,0 @@ -package complianceeventsapi - -import ( - "context" - "crypto/sha1" // #nosec G505 -- used for uniqueness checks and not for cryptography. - "database/sql" - "database/sql/driver" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "reflect" - "strings" - "time" - - "github.com/lib/pq" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1" -) - -var ( - errRequiredFieldNotProvided = errors.New("required field not provided") - errInvalidInput = errors.New("invalid input") - errDuplicateComplianceEvent = errors.New("the compliance event already exists") -) - -type dbRow interface { - InsertQuery() (string, []any) - SelectQuery(returnedColumns ...string) (string, []any) -} - -type metadata struct { - Page uint64 `json:"page"` - Pages uint64 `json:"pages"` - PerPage uint64 `json:"per_page"` //nolint:tagliatelle - Total uint64 `json:"total"` -} - -type ListResponse struct { - Data []ComplianceEvent `json:"data"` - Metadata metadata `json:"metadata"` -} - -type queryOptions struct { - ArrayFilters map[string][]string - Direction string - Filters map[string][]string - IncludeSpec bool - MessageIncludes string - MessageLike string - NullFilters []string - Page uint64 - PerPage uint64 - Sort []string - TimestampAfter time.Time - TimestampBefore time.Time -} - -type ComplianceEvent struct { - EventID int32 `json:"id"` - Cluster Cluster `json:"cluster"` - Event EventDetails `json:"event"` - ParentPolicy *ParentPolicy `json:"parent_policy"` //nolint:tagliatelle - Policy Policy `json:"policy"` -} - -// Validate ensures that a valid POST request for a compliance event is set. This means that if the shorthand approach -// of providing parent_policy.id and/or policy.id is used, the other fields for ParentPolicy and Policy will not be -// present. -func (ce ComplianceEvent) Validate(ctx context.Context, serverContext *ComplianceServerCtx) error { - errs := make([]error, 0) - - if err := ce.Cluster.Validate(); err != nil { - errs = append(errs, err) - } - - if ce.ParentPolicy != nil { - if ce.ParentPolicy.KeyID != 0 { - row := serverContext.DB.QueryRowContext( - ctx, `SELECT EXISTS(SELECT id FROM parent_policies WHERE id=$1);`, ce.ParentPolicy.KeyID, - ) - - var exists bool - - err := row.Scan(&exists) - if err != nil { - log.Error(err, "Failed to query for the existence of the parent policy ID", getPqErrKeyVals(err)...) - - return errors.New("failed to determine if parent_policy.id is valid") - } - - if exists { - // If the user provided extra data, ignore it since it won't be validated that it matches the database - ce.ParentPolicy = &ParentPolicy{KeyID: ce.ParentPolicy.KeyID} - } else { - errs = append(errs, fmt.Errorf("%w: parent_policy.id not found", errInvalidInput)) - } - } else if err := ce.ParentPolicy.Validate(); err != nil { - errs = append(errs, err) - } - } - - if err := ce.Event.Validate(); err != nil { - errs = append(errs, err) - } - - if ce.Policy.KeyID != 0 { - row := serverContext.DB.QueryRowContext( - ctx, `SELECT EXISTS(SELECT id FROM policies WHERE id=$1);`, ce.Policy.KeyID, - ) - - var exists bool - - err := row.Scan(&exists) - if err != nil { - log.Error(err, "Failed to query for the existence of the policy ID", getPqErrKeyVals(err)...) - - return errors.New("failed to determine if policy.id is valid") - } - - if exists { - // If the user provided extra data, ignore it since it won't be validated that it matches the database - ce.Policy = Policy{KeyID: ce.Policy.KeyID} - } else { - errs = append(errs, fmt.Errorf("%w: policy.id not found", errInvalidInput)) - } - } else if err := ce.Policy.Validate(); err != nil { - errs = append(errs, err) - } - - return errors.Join(errs...) -} - -func (ce *ComplianceEvent) Create(ctx context.Context, db *sql.DB) error { - if ce.Event.ClusterID == 0 { - ce.Event.ClusterID = ce.Cluster.KeyID - } - - if ce.Event.PolicyID == 0 { - ce.Event.PolicyID = ce.Policy.KeyID - } - - if ce.Event.ParentPolicyID == nil && ce.ParentPolicy != nil { - ce.Event.ParentPolicyID = &ce.ParentPolicy.KeyID - } - - insertQuery, insertArgs := ce.Event.InsertQuery() - - row := db.QueryRowContext( //nolint:execinquery - ctx, insertQuery+" ON CONFLICT DO NOTHING RETURNING id", insertArgs..., - ) - - err := row.Scan(&ce.Event.KeyID) - if err != nil { - // If this is true, then we know we encountered a conflict. This is simpler than parsing the unique constraint - // error. - if errors.Is(err, sql.ErrNoRows) { - return errDuplicateComplianceEvent - } - - return err - } - - return nil -} - -type Cluster struct { - KeyID int32 `db:"id" json:"-"` - Name string `db:"name" json:"name"` - ClusterID string `db:"cluster_id" json:"cluster_id"` //nolint:tagliatelle -} - -func (c Cluster) Validate() error { - errs := make([]error, 0) - - if c.Name == "" { - errs = append(errs, fmt.Errorf("%w: cluster.name", errRequiredFieldNotProvided)) - } - - if c.ClusterID == "" { - errs = append(errs, fmt.Errorf("%w: cluster.cluster_id", errRequiredFieldNotProvided)) - } - - return errors.Join(errs...) -} - -func (c *Cluster) InsertQuery() (string, []any) { - sql := `INSERT INTO clusters (cluster_id, name) VALUES ($1, $2)` - values := []any{c.ClusterID, c.Name} - - return sql, values -} - -func (c *Cluster) SelectQuery(returnedColumns ...string) (string, []any) { - if len(returnedColumns) == 0 { - returnedColumns = []string{"*"} - } - - sql := fmt.Sprintf( - `SELECT %s FROM clusters WHERE cluster_id=$1 AND name=$2`, - strings.Join(returnedColumns, ", "), - ) - values := []any{c.ClusterID, c.Name} - - return sql, values -} - -func (c *Cluster) GetOrCreate(ctx context.Context, db *sql.DB) error { - return getOrCreate(ctx, db, c) -} - -type EventDetails struct { - KeyID int32 `db:"id" json:"-"` - ClusterID int32 `db:"cluster_id" json:"-"` - PolicyID int32 `db:"policy_id" json:"-"` - ParentPolicyID *int32 `db:"parent_policy_id" json:"-"` - Compliance string `db:"compliance" json:"compliance"` - Message string `db:"message" json:"message"` - Timestamp time.Time `db:"timestamp" json:"timestamp"` - Metadata JSONMap `db:"metadata" json:"metadata"` - ReportedBy *string `db:"reported_by" json:"reported_by"` //nolint:tagliatelle -} - -func (e EventDetails) Validate() error { - errs := make([]error, 0) - - if e.Compliance == "" { - errs = append(errs, fmt.Errorf("%w: event.compliance", errRequiredFieldNotProvided)) - } else { - switch e.Compliance { - case "Compliant", "NonCompliant", "Disabled", "Pending": - default: - errs = append( - errs, - fmt.Errorf( - "%w: event.compliance should be Compliant, NonCompliant, Disabled, or Pending got %v", - errInvalidInput, e.Compliance, - ), - ) - } - } - - if e.Message == "" { - errs = append(errs, fmt.Errorf("%w: event.message", errRequiredFieldNotProvided)) - } - - if e.Timestamp.IsZero() { - errs = append(errs, fmt.Errorf("%w: event.timestamp", errRequiredFieldNotProvided)) - } - - return errors.Join(errs...) -} - -func (e *EventDetails) InsertQuery() (string, []any) { - hasher := sha1.New() // #nosec G401 -- used for uniqueness checks and not for cryptography. - hasher.Write([]byte(e.Message)) - messageHash := hex.EncodeToString(hasher.Sum(nil)) - - sql := `INSERT INTO compliance_events` + - `(cluster_id, compliance, message, message_hash, metadata, parent_policy_id, policy_id, reported_by, ` + - `timestamp) ` + - `VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)` - values := []any{ - e.ClusterID, - e.Compliance, - e.Message, - messageHash, - e.Metadata, - e.ParentPolicyID, - e.PolicyID, - e.ReportedBy, - e.Timestamp, - } - - return sql, values -} - -type ParentPolicy struct { - KeyID int32 `db:"id" json:"id"` - Name string `db:"name" json:"name"` - Namespace string `db:"namespace" json:"namespace"` - Categories pq.StringArray `db:"categories" json:"categories"` - Controls pq.StringArray `db:"controls" json:"controls"` - Standards pq.StringArray `db:"standards" json:"standards"` -} - -func (p ParentPolicy) Validate() error { - errs := []error{} - - if p.Name == "" { - errs = append(errs, fmt.Errorf("%w: parent_policy.name", errRequiredFieldNotProvided)) - } - - if p.Namespace == "" { - errs = append(errs, fmt.Errorf("%w: parent_policy.namespace", errRequiredFieldNotProvided)) - } - - return errors.Join(errs...) -} - -func (p *ParentPolicy) InsertQuery() (string, []any) { - sql := `INSERT INTO parent_policies` + - `(categories, controls, name, namespace, standards) ` + - `VALUES($1, $2, $3, $4, $5)` - values := []any{p.Categories, p.Controls, p.Name, p.Namespace, p.Standards} - - return sql, values -} - -func (p *ParentPolicy) SelectQuery(returnedColumns ...string) (string, []any) { - if len(returnedColumns) == 0 { - returnedColumns = []string{"*"} - } - - sql := fmt.Sprintf( - `SELECT %s FROM parent_policies WHERE name=$1 AND namespace=$2`, - strings.Join(returnedColumns, ", "), - ) - values := []any{p.Name, p.Namespace} - - columnCount := 2 - - if p.Categories == nil { - sql += " AND categories IS NULL" - } else { - columnCount++ - sql += fmt.Sprintf(" AND categories=$%d", columnCount) - values = append(values, p.Categories) - } - - if p.Controls == nil { - sql += " AND controls IS NULL" - } else { - columnCount++ - sql += fmt.Sprintf(" AND controls=$%d", columnCount) - values = append(values, p.Controls) - } - - if p.Standards == nil { - sql += " AND standards IS NULL" - } else { - columnCount++ - sql += fmt.Sprintf(" AND standards=$%d", columnCount) - values = append(values, p.Standards) - } - - return sql, values -} - -func (p *ParentPolicy) GetOrCreate(ctx context.Context, db *sql.DB) error { - return getOrCreate(ctx, db, p) -} - -func (p ParentPolicy) Key() string { - return fmt.Sprintf("%s;%s;%v;%v;%v", p.Namespace, p.Name, p.Categories, p.Controls, p.Standards) -} - -func ParentPolicyFromPolicyObj(plc *policiesv1.Policy) ParentPolicy { - annotations := plc.GetAnnotations() - categories := []string{} - - for _, category := range strings.Split(annotations["policy.open-cluster-management.io/categories"], ",") { - category = strings.TrimSpace(category) - if category != "" { - categories = append(categories, category) - } - } - - controls := []string{} - - for _, control := range strings.Split(annotations["policy.open-cluster-management.io/controls"], ",") { - control = strings.TrimSpace(control) - if control != "" { - controls = append(controls, control) - } - } - - standards := []string{} - - for _, standard := range strings.Split(annotations["policy.open-cluster-management.io/standards"], ",") { - standard = strings.TrimSpace(standard) - if standard != "" { - standards = append(standards, standard) - } - } - - return ParentPolicy{ - Name: plc.Name, - Namespace: plc.Namespace, - Categories: categories, - Controls: controls, - Standards: standards, - } -} - -type Spec struct { - KeyID int32 `db:"id" json:"id"` - Spec JSONMap `json:"spec,omitempty"` -} - -func (s *Spec) InsertQuery() (string, []any) { - sql := `INSERT INTO specs(spec) VALUES($1)` - values := []any{s.Spec} - - return sql, values -} - -func (s *Spec) SelectQuery(returnedColumns ...string) (string, []any) { - if len(returnedColumns) == 0 { - returnedColumns = []string{"*"} - } - - sql := fmt.Sprintf( - `SELECT %s FROM specs WHERE spec=$1`, - strings.Join(returnedColumns, ", "), - ) - - values := []any{s.Spec} - - return sql, values -} - -func (s *Spec) GetOrCreate(ctx context.Context, db *sql.DB) error { - return getOrCreate(ctx, db, s) -} - -func PolicyFromUnstructured(obj unstructured.Unstructured) *Policy { - policy := &Policy{} - - policy.APIGroup = obj.GetAPIVersion() - policy.Kind = obj.GetKind() - ns := obj.GetNamespace() - - if ns != "" { - policy.Namespace = &ns - } - - policy.Name = obj.GetName() - - spec, ok, _ := unstructured.NestedMap(obj.Object, "spec") - if ok { - typedSpec := JSONMap{} - - for key, val := range spec { - typedSpec[key] = val - } - - policy.Spec = typedSpec - } - - if severity, ok := spec["severity"]; ok { - if sevString, ok := severity.(string); ok { - policy.Severity = &sevString - } - } - - return policy -} - -type Policy struct { - KeyID int32 `db:"id" json:"id"` - Kind string `db:"kind" json:"kind"` - APIGroup string `db:"api_group" json:"apiGroup"` - Name string `db:"name" json:"name"` - Namespace *string `db:"namespace" json:"namespace"` - Spec JSONMap `json:"spec,omitempty"` - SpecID int32 `db:"spec_id" json:"-"` - Severity *string `db:"severity" json:"severity"` -} - -func (p *Policy) Validate() error { - errs := make([]error, 0) - - if p.APIGroup == "" { - errs = append(errs, fmt.Errorf("%w: policy.apiGroup", errRequiredFieldNotProvided)) - } - - if p.Kind == "" { - errs = append(errs, fmt.Errorf("%w: policy.kind", errRequiredFieldNotProvided)) - } - - if p.Name == "" { - errs = append(errs, fmt.Errorf("%w: policy.name", errRequiredFieldNotProvided)) - } - - if p.Spec == nil { - errs = append(errs, fmt.Errorf("%w: policy.spec", errRequiredFieldNotProvided)) - } - - return errors.Join(errs...) -} - -func (p *Policy) InsertQuery() (string, []any) { - sql := `INSERT INTO policies` + - `(api_group, kind, name, namespace, severity, spec_id) ` + - `VALUES($1, $2, $3, $4, $5, $6)` - values := []any{p.APIGroup, p.Kind, p.Name, p.Namespace, p.Severity, p.SpecID} - - return sql, values -} - -func (p *Policy) SelectQuery(returnedColumns ...string) (string, []any) { - if len(returnedColumns) == 0 { - returnedColumns = []string{ - "policies.id", - "policies.kind", - "policies.api_group", - "policies.name", - "policies.namespace", - "policies.severity", - "specs.spec", - } - } else { - for i, column := range returnedColumns { - if column == "id" { - returnedColumns[i] = "policies.id" - - break - } - } - } - - sql := fmt.Sprintf( - `SELECT %s FROM policies `+ - `LEFT JOIN specs ON policies.spec_id=specs.id`+ - ` WHERE api_group=$1 AND kind=$2 AND name=$3 AND spec=$4`, - strings.Join(returnedColumns, ", "), - ) - - values := []any{p.APIGroup, p.Kind, p.Name, p.Spec} - - columnCount := 4 - - if p.Namespace == nil { - sql += " AND namespace is NULL" - } else { - columnCount++ - sql += fmt.Sprintf(" AND namespace=$%d", columnCount) - values = append(values, p.Namespace) - } - - if p.Severity == nil { - sql += " AND severity is NULL" - } else { - columnCount++ - sql += fmt.Sprintf(" AND severity=$%d", columnCount) - values = append(values, p.Severity) - } - - return sql, values -} - -func (p *Policy) GetOrCreate(ctx context.Context, db *sql.DB) error { - if p.SpecID == 0 { - spec := &Spec{Spec: p.Spec} - if err := getOrCreate(ctx, db, spec); err != nil { - return err - } - - p.SpecID = spec.KeyID - } - - return getOrCreate(ctx, db, p) -} - -func (p *Policy) Key() string { - var namespace string - - if p.Namespace != nil { - namespace = *p.Namespace - } - - var severity string - - if p.Severity != nil { - severity = *p.Severity - } - - // Note that as of Go 1.20, it sorts the keys in the underlying map of p.Spec, which is why this is deterministic. - // https://github.com/golang/go/blob/97c8ff8d53759e7a82b1862403df1694f2b6e073/src/fmt/print.go#L816-L828 - return fmt.Sprintf("%s;%s;%s;%v;%v;%v", p.APIGroup, p.Kind, p.Name, namespace, severity, p.Spec) -} - -type JSONMap map[string]interface{} - -// Value returns a value that the database driver can use, or an error. -func (j JSONMap) Value() (driver.Value, error) { - return json.Marshal(j) -} - -// Scan allows for reading a JSONMap from the database. -func (j *JSONMap) Scan(src interface{}) error { - var source []byte - - switch s := src.(type) { - case string: - source = []byte(s) - case []byte: - source = s - case nil: - source = nil - default: - return errors.New("incompatible type for JSONMap") - } - - return json.Unmarshal(source, j) -} - -// getOrCreate will translate the input object to an INSERT SQL query. When the input object already exists in the -// database, a SELECT query is performed. The primary key is set on the input object when it is inserted or gotten -// from the database. The INSERT first then SELECT approach is a clean way to account for race conditions of multiple -// goroutines creating the same row. -func getOrCreate(ctx context.Context, db *sql.DB, obj dbRow) error { - insertQuery, insertArgs := obj.InsertQuery() - - // On inserts, it returns the primary key value (e.g. id). If it already exists, nothing is returned. - row := db.QueryRowContext(ctx, fmt.Sprintf(`%s ON CONFLICT DO NOTHING RETURNING "id"`, insertQuery), insertArgs...) - - var primaryKey int32 - - err := row.Scan(&primaryKey) - if errors.Is(err, sql.ErrNoRows) { - // The insertion did not return anything, so that means the value already exists, so perform a SELECT query - // just using the unique columns. No more information is needed to get the right row and it allows the unique - // indexes to be used to make the query more efficient. - selectQuery, selectArgs := obj.SelectQuery("id") - - row = db.QueryRowContext(ctx, selectQuery, selectArgs...) - - err = row.Scan(&primaryKey) - if err != nil { - return err - } - } else if err != nil { - return err - } - - // Set the primary key value on the object - values := reflect.Indirect(reflect.ValueOf(obj)) - values.FieldByName("KeyID").Set(reflect.ValueOf(primaryKey)) - - return nil -} diff --git a/controllers/complianceeventsapi/types_test.go b/controllers/complianceeventsapi/types_test.go deleted file mode 100644 index c725e372..00000000 --- a/controllers/complianceeventsapi/types_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package complianceeventsapi - -import ( - "strings" - "testing" - "time" -) - -func TestClusterValidation(t *testing.T) { - tests := map[string]struct { - obj Cluster - errMsg string - }{ - "no name": {Cluster{ClusterID: "foo"}, "field not provided: cluster.name"}, - "no cluster id": {Cluster{Name: "foo"}, "field not provided: cluster.cluster_id"}, - } - - for input, tc := range tests { - t.Run(input, func(t *testing.T) { - err := tc.obj.Validate() - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), tc.errMsg) { - t.Fatal("expected error to include", tc.errMsg, "in the error string; got", err.Error()) - } - }) - } -} - -func TestEventDetailsValidation(t *testing.T) { - tests := map[string]struct { - obj EventDetails - errMsg string - }{ - "no compliance": { - EventDetails{Message: "hello", Timestamp: time.Now()}, - "field not provided: event.compliance", - }, - "bad compliance": { - EventDetails{Compliance: "bad", Message: "hello", Timestamp: time.Now()}, - "should be Compliant, NonCompliant, Disabled, or Pending got bad", - }, - "no message": { - EventDetails{Compliance: "Compliant", Timestamp: time.Now()}, - "field not provided: event.message", - }, - "no timestamp": { - EventDetails{Compliance: "Compliant", Message: "hello"}, - "field not provided: event.timestamp", - }, - } - - for input, tc := range tests { - t.Run(input, func(t *testing.T) { - err := tc.obj.Validate() - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), tc.errMsg) { - t.Fatal("expected error to include", tc.errMsg, "in the error string; got", err.Error()) - } - }) - } -} - -func TestParentPolicyValidation(t *testing.T) { - tests := map[string]struct { - obj ParentPolicy - errMsg string - }{ - "no name": { - ParentPolicy{Namespace: "policies", Categories: []string{"hello"}}, - "field not provided: parent_policy.name", - }, - "no namespace": { - ParentPolicy{Name: "my-policy", Categories: []string{"hello"}}, - "field not provided: parent_policy.namespace", - }, - } - - for input, tc := range tests { - t.Run(input, func(t *testing.T) { - err := tc.obj.Validate() - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), tc.errMsg) { - t.Fatal("expected error to include", tc.errMsg, "in the error string; got", err.Error()) - } - }) - } -} - -func TestPolicyValidation(t *testing.T) { - var basespec JSONMap = map[string]interface{}{"test": "one", "severity": "low"} - - tests := map[string]struct { - obj Policy - errMsg string - }{ - "no name": { - Policy{ - Kind: "policy", - APIGroup: "v1", - Spec: basespec, - }, - "field not provided: policy.name", - }, - "no API group": { - Policy{ - Kind: "policy", - Name: "foobar", - Spec: basespec, - }, - "field not provided: policy.apiGroup", - }, - "no kind": { - Policy{ - APIGroup: "v1", - Name: "foobar", - Spec: basespec, - }, - "field not provided: policy.kind", - }, - "no spec or hash": { - Policy{ - Kind: "policy", - APIGroup: "v1", - Name: "foobar", - }, - "field not provided: policy.spec", - }, - } - - for input, tc := range tests { - t.Run(input, func(t *testing.T) { - err := tc.obj.Validate() - if err == nil { - t.Fatal("expected error") - } - - if !strings.Contains(err.Error(), tc.errMsg) { - t.Fatal("expected error to include", tc.errMsg, "in the error string; got", err.Error()) - } - }) - } -} diff --git a/controllers/propagator/replicatedpolicy_controller.go b/controllers/propagator/replicatedpolicy_controller.go index 98026622..c94e4123 100644 --- a/controllers/propagator/replicatedpolicy_controller.go +++ b/controllers/propagator/replicatedpolicy_controller.go @@ -4,13 +4,11 @@ import ( "context" "errors" "fmt" - "strconv" "strings" "sync" k8sdepwatches "github.com/stolostron/kubernetes-dependency-watches/client" k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1" @@ -20,22 +18,15 @@ import ( policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1" "open-cluster-management.io/governance-policy-propagator/controllers/common" - "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" -) - -const ( - ParentPolicyIDAnnotation = "policy.open-cluster-management.io/parent-policy-compliance-db-id" - PolicyIDAnnotation = "policy.open-cluster-management.io/policy-compliance-db-id" ) var _ reconcile.Reconciler = &ReplicatedPolicyReconciler{} type ReplicatedPolicyReconciler struct { Propagator - ResourceVersions *sync.Map - DynamicWatcher k8sdepwatches.DynamicWatcher - ComplianceServerCtx *complianceeventsapi.ComplianceServerCtx - TemplateResolvers *TemplateResolvers + ResourceVersions *sync.Map + DynamicWatcher k8sdepwatches.DynamicWatcher + TemplateResolvers *TemplateResolvers } func (r *ReplicatedPolicyReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { @@ -265,8 +256,6 @@ func (r *ReplicatedPolicyReconciler) Reconcile(ctx context.Context, request ctrl objsToWatch[saObjID] = true } - r.setDBAnnotations(ctx, rootPolicy, desiredReplicatedPolicy, replicatedPolicy) - if len(objsToWatch) != 0 { refObjs := make([]k8sdepwatches.ObjectIdentifier, 0, len(objsToWatch)) for objToWatch := range objsToWatch { @@ -351,261 +340,6 @@ func (r *ReplicatedPolicyReconciler) Reconcile(ctx context.Context, request ctrl return reconcile.Result{}, returnErr } -// getParentPolicyID needs to have the caller call r.ComplianceServerCtx.Lock.RLock. -func (r *ReplicatedPolicyReconciler) getParentPolicyID( - ctx context.Context, - rootPolicy *policiesv1.Policy, - existingReplicatedPolicy *policiesv1.Policy, -) (int32, error) { - dbParentPolicy := complianceeventsapi.ParentPolicyFromPolicyObj(rootPolicy) - - // Check the cache first. - cachedParentPolicyID, ok := r.ComplianceServerCtx.ParentPolicyToID.Load(dbParentPolicy.Key()) - if ok { - return cachedParentPolicyID.(int32), nil - } - - // Try the database second before checking the replicated policy to be able to recover if the compliance history - // database is restored from backup and the IDs on the replicated policy no longer exist. - var dbErr error - - if r.ComplianceServerCtx.DB != nil { - err := dbParentPolicy.GetOrCreate(ctx, r.ComplianceServerCtx.DB) - if err == nil { - r.ComplianceServerCtx.ParentPolicyToID.Store(dbParentPolicy.Key(), dbParentPolicy.KeyID) - - return dbParentPolicy.KeyID, nil - } - - if r.ComplianceServerCtx.DB.PingContext(ctx) != nil { - dbErr = complianceeventsapi.ErrDBConnectionFailed - } else { - dbErr = fmt.Errorf("%w: failed to get the database ID of the parent policy", err) - } - } else { - dbErr = complianceeventsapi.ErrDBConnectionFailed - } - - // Check if the existing replicated policy already has the annotation set and has the same - // categories, controls, and standards as the current root policy. - var parentPolicyIDFromRepl string - if existingReplicatedPolicy != nil { - parentPolicyIDFromRepl = existingReplicatedPolicy.Annotations[ParentPolicyIDAnnotation] - } - - if parentPolicyIDFromRepl != "" { - dbParentPolicyFromRepl := complianceeventsapi.ParentPolicyFromPolicyObj(existingReplicatedPolicy) - dbParentPolicyFromRepl.Name = rootPolicy.Name - dbParentPolicyFromRepl.Namespace = rootPolicy.Namespace - - if dbParentPolicy.Key() == dbParentPolicyFromRepl.Key() { - parentPolicyID, err := strconv.ParseInt(parentPolicyIDFromRepl, 10, 32) - if err == nil && parentPolicyID != 0 { - r.ComplianceServerCtx.ParentPolicyToID.Store(dbParentPolicy.Key(), int32(parentPolicyID)) - - return int32(parentPolicyID), nil - } - } - } - - return 0, dbErr -} - -// getPolicyID needs to have the caller call r.ComplianceServerCtx.Lock.RLock. -func (r *ReplicatedPolicyReconciler) getPolicyID( - ctx context.Context, - replPolicy *policiesv1.Policy, - existingReplPolicy *policiesv1.Policy, - replTemplateIdx int, - skipDB bool, -) (int32, *unstructured.Unstructured, error) { - // Start by checking the cache. - plcTemplate := replPolicy.Spec.PolicyTemplates[replTemplateIdx] - plcTmplUnstruct := &unstructured.Unstructured{} - - err := plcTmplUnstruct.UnmarshalJSON(plcTemplate.ObjectDefinition.Raw) - if err != nil { - return 0, plcTmplUnstruct, err - } - - dbPolicy := complianceeventsapi.PolicyFromUnstructured(*plcTmplUnstruct) - if err := dbPolicy.Validate(); err != nil { - return 0, plcTmplUnstruct, err - } - - var policyID int32 - - cachedPolicyID, ok := r.ComplianceServerCtx.PolicyToID.Load(dbPolicy.Key()) - if ok { - policyID = cachedPolicyID.(int32) - - return policyID, plcTmplUnstruct, nil - } - - // Try the database second before checking the replicated policy to be able to recover if the compliance history - // database is restored from backup and the IDs on the replicated policy no longer exist. - var dbErr error - if skipDB || r.ComplianceServerCtx.DB == nil { - dbErr = complianceeventsapi.ErrDBConnectionFailed - } else { - err = dbPolicy.GetOrCreate(ctx, r.ComplianceServerCtx.DB) - if err == nil { - r.ComplianceServerCtx.PolicyToID.Store(dbPolicy.Key(), dbPolicy.KeyID) - - return dbPolicy.KeyID, plcTmplUnstruct, nil - } - - dbErr = err - } - - // Check if the existing policy template matches the existing one - var existingPlcTemplate *policiesv1.PolicyTemplate - - existingPlcTmplUnstruct := unstructured.Unstructured{} - - var existingAnnotation string - var existingDBPolicy *complianceeventsapi.Policy - - // Try the existing policy template first before trying the database. - if existingReplPolicy != nil && len(existingReplPolicy.Spec.PolicyTemplates) >= replTemplateIdx+1 { - existingPlcTemplate = existingReplPolicy.Spec.PolicyTemplates[replTemplateIdx] - - err = existingPlcTmplUnstruct.UnmarshalJSON(existingPlcTemplate.ObjectDefinition.Raw) - if err == nil { - existingAnnotations := existingPlcTmplUnstruct.GetAnnotations() - existingAnnotation = existingAnnotations[PolicyIDAnnotation] - - if existingAnnotation != "" { - existingDBPolicy = complianceeventsapi.PolicyFromUnstructured(existingPlcTmplUnstruct) - } - } - } - - // This is a continuation from the above if statement but this was broken up here to make it less indented. - if existingAnnotation != "" { - if err := existingDBPolicy.Validate(); err == nil { - if dbPolicy.Key() == existingDBPolicy.Key() { - policyID, err := strconv.ParseInt(existingAnnotation, 10, 32) - if err == nil && policyID != 0 { - r.ComplianceServerCtx.PolicyToID.Store(dbPolicy.Key(), int32(policyID)) - - return int32(policyID), plcTmplUnstruct, nil - } - } - } - } - - return 0, plcTmplUnstruct, dbErr -} - -// setDBAnnotations sets the parent policy ID on the replicated policy and the policy ID for each policy template. -// If the DB connection is unavailable, it queues up a reconcile for when the DB becomes available. -func (r *ReplicatedPolicyReconciler) setDBAnnotations( - ctx context.Context, - rootPolicy *policiesv1.Policy, - replicatedPolicy *policiesv1.Policy, - existingReplicatedPolicy *policiesv1.Policy, -) { - r.ComplianceServerCtx.Lock.RLock() - defer r.ComplianceServerCtx.Lock.RUnlock() - - // Assume the database is connected unless told otherwise. - dbAvailable := true - var requeueForDB bool - - annotations := replicatedPolicy.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - - parentPolicyID, err := r.getParentPolicyID(ctx, rootPolicy, existingReplicatedPolicy) - if err != nil { - if errors.Is(err, complianceeventsapi.ErrDBConnectionFailed) { - dbAvailable = false - } else { - log.Error( - err, "Failed to get the parent policy ID", "name", rootPolicy.Name, "namespace", rootPolicy.Namespace, - ) - } - - requeueForDB = true - - // Remove it if the user accidentally provided the annotation - if annotations[ParentPolicyIDAnnotation] != "" { - delete(annotations, PolicyIDAnnotation) - replicatedPolicy.SetAnnotations(annotations) - } - } else { - annotations[ParentPolicyIDAnnotation] = strconv.FormatInt(int64(parentPolicyID), 10) - replicatedPolicy.SetAnnotations(annotations) - } - - for i, plcTemplate := range replicatedPolicy.Spec.PolicyTemplates { - if plcTemplate == nil { - continue - } - - policyID, plcTmplUnstruct, err := r.getPolicyID( - ctx, replicatedPolicy, existingReplicatedPolicy, i, !dbAvailable, - ) - if err != nil { - if errors.Is(err, complianceeventsapi.ErrDBConnectionFailed) { - dbAvailable = false - } else { - log.Error( - err, - "Failed to get the policy ID for the policy template", - "name", plcTmplUnstruct.GetName(), - "namespace", plcTmplUnstruct.GetNamespace(), - "index", i, - ) - } - - requeueForDB = true - tmplAnnotations := plcTmplUnstruct.GetAnnotations() - - if tmplAnnotations[PolicyIDAnnotation] == "" { - continue - } - - // Remove it if the user accidentally provided the annotation - delete(tmplAnnotations, PolicyIDAnnotation) - plcTmplUnstruct.SetAnnotations(tmplAnnotations) - } else { - tmplAnnotations := plcTmplUnstruct.GetAnnotations() - if tmplAnnotations == nil { - tmplAnnotations = map[string]string{} - } - - tmplAnnotations[PolicyIDAnnotation] = strconv.FormatInt(int64(policyID), 10) - plcTmplUnstruct.SetAnnotations(tmplAnnotations) - } - - updatedTemplate, err := plcTmplUnstruct.MarshalJSON() - if err != nil { - log.Error( - err, "Failed to set the annotation on the policy template", "index", i, "anotation", PolicyIDAnnotation, - ) - - continue - } - - replicatedPolicy.Spec.PolicyTemplates[i].ObjectDefinition.Raw = updatedTemplate - } - - if requeueForDB { - log.V(2).Info( - "The compliance events database is not available. Queuing this replicated policy to be reprocessed if the "+ - "database becomes available.", - "namespace", replicatedPolicy.Namespace, - "name", replicatedPolicy.Name, - ) - r.ComplianceServerCtx.Queue.Add( - types.NamespacedName{Namespace: replicatedPolicy.Namespace, Name: replicatedPolicy.Name}, - ) - } -} - func (r *ReplicatedPolicyReconciler) cleanUpReplicated(ctx context.Context, replicatedPolicy *policiesv1.Policy) error { gvk := replicatedPolicy.GroupVersionKind() diff --git a/controllers/propagator/replicatedpolicy_controller_test.go b/controllers/propagator/replicatedpolicy_controller_test.go deleted file mode 100644 index 63b9d642..00000000 --- a/controllers/propagator/replicatedpolicy_controller_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package propagator - -import ( - "context" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - - policiesv1 "open-cluster-management.io/governance-policy-propagator/api/v1" - "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" -) - -func getPolicyTemplateAnnotations(policy *policiesv1.Policy, templateIndex int) (map[string]string, error) { - plcTmplUnstruct := &unstructured.Unstructured{} - - err := plcTmplUnstruct.UnmarshalJSON(policy.Spec.PolicyTemplates[templateIndex].ObjectDefinition.Raw) - if err != nil { - return nil, err - } - - return plcTmplUnstruct.GetAnnotations(), nil -} - -func TestSetDBAnnotationsNoDB(t *testing.T) { - complianceAPICtx, err := complianceeventsapi.NewComplianceServerCtx("postgres://localhost?mydb", "unknown") - if err != nil { - t.Fatalf("Failed create the compliance server context: %v", err) - } - - // The unit tests shouldn't use the database, so that part of the code can't be covered here. - complianceAPICtx.DB = nil - - reconciler := ReplicatedPolicyReconciler{ - ComplianceServerCtx: complianceAPICtx, - } - - // Test no cache entry, no existing annotation on the replicated policy, and no database connection - rootPolicy := &policiesv1.Policy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-policy", - Namespace: "policies", - Annotations: map[string]string{ - "policy.open-cluster-management.io/categories": "category1", - "policy.open-cluster-management.io/controls": "controls1, controls2", - "policy.open-cluster-management.io/standards": "standard1", - }, - }, - Spec: policiesv1.PolicySpec{ - PolicyTemplates: []*policiesv1.PolicyTemplate{ - { - ObjectDefinition: runtime.RawExtension{ - Raw: []byte(`{ - "apiVersion": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "metadata": { - "name": "my-config", - "annotations": {} - }, - "spec": { - "severity": "critical", - "option1": "option2" - } - }`), - }, - }, - }, - }, - } - - replicatedPolicy := rootPolicy.DeepCopy() - - existingReplicatedPolicy := replicatedPolicy.DeepCopy() - - reconciler.setDBAnnotations(context.TODO(), rootPolicy, replicatedPolicy, existingReplicatedPolicy) - - annotations := rootPolicy.GetAnnotations() - if annotations[ParentPolicyIDAnnotation] != "" { - t.Fatalf("Expected no parent policy annotation but got: %s", annotations[ParentPolicyIDAnnotation]) - } - - templateAnnotations, err := getPolicyTemplateAnnotations(replicatedPolicy, 0) - if err != nil { - t.Fatalf("Expected to get the policy template annotations but got: %v", err) - } - - if templateAnnotations[PolicyIDAnnotation] != "" { - t.Fatalf("Expected no policy annotation but got: %s", templateAnnotations[PolicyIDAnnotation]) - } - - // Test an existing replicated policy with annotations - rootPolicy2 := rootPolicy.DeepCopy() - replicatedPolicy2 := rootPolicy2.DeepCopy() - existingReplicatedPolicy2 := rootPolicy2.DeepCopy() - - existingReplicatedPolicy2.Annotations["policy.open-cluster-management.io/parent-policy-compliance-db-id"] = "23" - existingReplicatedPolicy2.Spec.PolicyTemplates[0].ObjectDefinition.Raw = []byte(`{ - "apiVersion": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "metadata": { - "name": "my-config", - "annotations": { - "policy.open-cluster-management.io/policy-compliance-db-id": "56" - } - }, - "spec": { - "severity": "critical", - "option1": "option2" - } - }`) - - reconciler.setDBAnnotations(context.TODO(), rootPolicy2, replicatedPolicy2, existingReplicatedPolicy2) - - annotations = replicatedPolicy2.GetAnnotations() - if annotations[ParentPolicyIDAnnotation] != "23" { - t.Fatalf("Expected the parent policy ID of 23 but got: %s", annotations[ParentPolicyIDAnnotation]) - } - - templateAnnotations, err = getPolicyTemplateAnnotations(replicatedPolicy2, 0) - if err != nil { - t.Fatalf("Expected to get the policy template annotations but got: %v", err) - } - - if templateAnnotations[PolicyIDAnnotation] != "56" { - t.Fatalf("Expected the policy ID of 56 but got: %s", templateAnnotations[PolicyIDAnnotation]) - } - - if len(templateAnnotations) != 1 { - t.Fatalf("Expected 1 policy annotation but got: %d", len(templateAnnotations)) - } - - // Test a cache hit from the last run using the policies from the first run - reconciler.setDBAnnotations(context.TODO(), rootPolicy, replicatedPolicy, existingReplicatedPolicy) - - annotations = replicatedPolicy.GetAnnotations() - if annotations[ParentPolicyIDAnnotation] != "23" { - t.Fatalf("Expected the parent policy ID of 23 but got: %s", annotations[ParentPolicyIDAnnotation]) - } - - templateAnnotations, err = getPolicyTemplateAnnotations(replicatedPolicy, 0) - if err != nil { - t.Fatalf("Expected to get the policy template annotations but got: %v", err) - } - - if templateAnnotations[PolicyIDAnnotation] != "56" { - t.Fatalf("Expected the policy ID of 56 but got: %s", templateAnnotations[PolicyIDAnnotation]) - } - - if len(templateAnnotations) != 1 { - t.Fatalf("Expected 1 policy annotation but got: %d", len(templateAnnotations)) - } -} diff --git a/deploy/manager/manager.yaml b/deploy/manager/manager.yaml index 879744f4..ab2dc792 100644 --- a/deploy/manager/manager.yaml +++ b/deploy/manager/manager.yaml @@ -29,14 +29,10 @@ spec: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=:8383" - "--leader-elect" - - "--compliance-history-api-host=0.0.0.0" ports: - containerPort: 8383 protocol: TCP name: http - - containerPort: 8384 - protocol: TCP - name: compliance-api - containerPort: 9443 protocol: TCP name: webhook-http @@ -54,24 +50,8 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "governance-policy-propagator" - - name: WATCH_NAMESPACE_COMPLIANCE_EVENTS_STORE - valueFrom: - fieldRef: - fieldPath: metadata.namespace volumes: - name: cert secret: defaultMode: 420 secretName: propagator-webhook-server-cert ---- -apiVersion: v1 -kind: Service -metadata: - name: governance-compliance-api -spec: - ports: - - port: 8384 - protocol: TCP - targetPort: 8384 - selector: - name: governance-policy-propagator diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 4ec0cddc..40b34de4 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -46,16 +46,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resourceNames: - - governance-policy-database - resources: - - secrets - verbs: - - get - - list - - watch - apiGroups: - "" resourceNames: @@ -93,7 +83,6 @@ rules: - apiGroups: - authorization.k8s.io resources: - - subjectaccessreviews - tokenreviews verbs: - create @@ -188,18 +177,6 @@ subjects: name: governance-policy-propagator namespace: open-cluster-management --- -apiVersion: v1 -kind: Service -metadata: - name: governance-compliance-api -spec: - ports: - - port: 8384 - protocol: TCP - targetPort: 8384 - selector: - name: governance-policy-propagator ---- apiVersion: apps/v1 kind: Deployment metadata: @@ -225,7 +202,6 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=:8383 - --leader-elect - - --compliance-history-api-host=0.0.0.0 command: - governance-policy-propagator env: @@ -237,10 +213,6 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: governance-policy-propagator - - name: WATCH_NAMESPACE_COMPLIANCE_EVENTS_STORE - valueFrom: - fieldRef: - fieldPath: metadata.namespace image: quay.io/open-cluster-management/governance-policy-propagator:latest imagePullPolicy: Always name: governance-policy-propagator @@ -248,9 +220,6 @@ spec: - containerPort: 8383 name: http protocol: TCP - - containerPort: 8384 - name: compliance-api - protocol: TCP - containerPort: 9443 name: webhook-http protocol: TCP diff --git a/deploy/rbac/role.yaml b/deploy/rbac/role.yaml index 299aef34..3fcace7c 100644 --- a/deploy/rbac/role.yaml +++ b/deploy/rbac/role.yaml @@ -16,16 +16,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resourceNames: - - governance-policy-database - resources: - - secrets - verbs: - - get - - list - - watch - apiGroups: - "" resourceNames: @@ -63,7 +53,6 @@ rules: - apiGroups: - authorization.k8s.io resources: - - subjectaccessreviews - tokenreviews verbs: - create diff --git a/go.mod b/go.mod index 77bbb5c4..aff8343e 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,7 @@ go 1.22.0 require ( github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-logr/zapr v1.3.0 - github.com/golang-migrate/migrate/v4 v4.17.1 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.6.0 - github.com/lib/pq v1.10.9 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 github.com/prometheus/client_golang v1.19.1 @@ -16,7 +13,6 @@ require ( github.com/stolostron/go-log-utils v0.1.2 github.com/stolostron/go-template-utils/v6 v6.3.0 github.com/stolostron/kubernetes-dependency-watches v0.10.0 - github.com/stolostron/rbac-api-utils v0.0.0-20240404212618-7f57fc664256 k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.0 @@ -55,9 +51,8 @@ require ( github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect + github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -86,7 +81,6 @@ require ( go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.24.0 // indirect diff --git a/go.sum b/go.sum index fb7191b2..b18ff145 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= @@ -7,8 +5,6 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= @@ -26,18 +22,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= -github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= -github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= @@ -72,8 +56,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= -github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -95,11 +77,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -118,8 +95,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -128,25 +103,17 @@ github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HK github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -181,8 +148,6 @@ github.com/stolostron/go-template-utils/v6 v6.3.0 h1:ptbIZcJqcWtT8baOrQyoqVFGj1P github.com/stolostron/go-template-utils/v6 v6.3.0/go.mod h1:4+HWMKPMBDDekJlPve3zkuWdE0hcwgnKKOap2GoHjNY= github.com/stolostron/kubernetes-dependency-watches v0.10.0 h1:brg9FCZUvd1gnm5wmsv/InfErcPUvYcZsK/LWNRr+wg= github.com/stolostron/kubernetes-dependency-watches v0.10.0/go.mod h1:j1DBv/3JjwDX3bT/oKB4YvSwJ6DEVcrUpEzKbFLM0QM= -github.com/stolostron/rbac-api-utils v0.0.0-20240404212618-7f57fc664256 h1:BeTUZoAkKzPKSH0sG4a9PaakKHuJ0h9Cks9joBn3Ns8= -github.com/stolostron/rbac-api-utils v0.0.0-20240404212618-7f57fc664256/go.mod h1:zYGYkVgY+sL501na1x5RDCKMrHD+JAwb6oRFU8e9XlU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -210,8 +175,6 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+ go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -230,8 +193,6 @@ golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/main.go b/main.go index 896b3f03..5e31d7b2 100644 --- a/main.go +++ b/main.go @@ -5,11 +5,8 @@ package main import ( "context" - "crypto/tls" - "errors" "flag" "fmt" - "net" "os" "runtime" "strconv" @@ -24,16 +21,13 @@ import ( corev1 "k8s.io/api/core/v1" k8serrors "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/fields" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/client-go/rest" "k8s.io/klog/v2" clusterv1 "open-cluster-management.io/api/cluster/v1" clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" @@ -53,7 +47,6 @@ import ( policyv1 "open-cluster-management.io/governance-policy-propagator/api/v1" policyv1beta1 "open-cluster-management.io/governance-policy-propagator/api/v1beta1" automationctrl "open-cluster-management.io/governance-policy-propagator/controllers/automation" - "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" encryptionkeysctrl "open-cluster-management.io/governance-policy-propagator/controllers/encryptionkeys" metricsctrl "open-cluster-management.io/governance-policy-propagator/controllers/policymetrics" policysetctrl "open-cluster-management.io/governance-policy-propagator/controllers/policyset" @@ -63,13 +56,8 @@ import ( ) var ( - scheme = k8sruntime.NewScheme() - log = ctrl.Log.WithName("setup") - clusterClaimGVR = schema.GroupVersionResource{ - Group: "cluster.open-cluster-management.io", - Version: "v1alpha1", - Resource: "clusterclaims", - } + scheme = k8sruntime.NewScheme() + log = ctrl.Log.WithName("setup") crdGVR = schema.GroupVersionResource{ Group: "apiextensions.k8s.io", Version: "v1", @@ -123,10 +111,6 @@ func main() { rootPolicyMaxConcurrency uint16 replPolicyMaxConcurrency uint16 enableWebhooks bool - complianceAPIHost string - complianceAPIPort string - complianceAPICert string - complianceAPIKey string disablePlacementRule bool ) @@ -179,23 +163,6 @@ func main() { 10, "The maximum number of concurrent reconciles for the replicated-policy controller", ) - pflag.StringVar( - &complianceAPIHost, "compliance-history-api-host", "localhost", - "The hostname that the event history API will listen on", - ) - pflag.StringVar( - &complianceAPIPort, "compliance-history-api-port", "8384", - "The port that the compliance history API will listen on", - ) - pflag.StringVar( - &complianceAPICert, "compliance-history-api-cert", "", - "The path to the certificate the compliance history API will use for HTTPS (CA cert, if any, concatenated "+ - "after server cert). If not set, HTTP will be used.", - ) - pflag.StringVar( - &complianceAPIKey, "compliance-history-api-key", "", - "The path to the private key the compliance history API will use for HTTPS. If not set, HTTP will be used.", - ) pflag.BoolVar(&disablePlacementRule, "disable-placementrule", false, "Disable watches for PlacementRules.") @@ -400,25 +367,6 @@ func main() { dynamicClient := dynamic.NewForConfigOrDie(mgr.GetConfig()) - var clusterID string - - idClusterClaim, err := dynamicClient.Resource(clusterClaimGVR).Get(controllerCtx, "id.k8s.io", metav1.GetOptions{}) - if err != nil && !k8serrors.IsNotFound(err) { - log.Error(err, "Failed to find the cluster ID") - - os.Exit(1) - } - - if err == nil { - clusterID, _, _ = unstructured.NestedString(idClusterClaim.Object, "spec", "value") - } - - if clusterID == "" { - log.Info("The id.k8s.io cluster claim is not set. Using the cluster ID of unknown.") - - clusterID = "unknown" - } - // Only check for the CRD if the flag was not set explicitly. if !pflag.Lookup("disable-placementrule").Changed { _, err = dynamicClient.Resource(crdGVR).Get( @@ -512,55 +460,10 @@ func main() { // This is important to avoid adding watches before the dynamic watcher is ready <-dynamicWatcher.Started() - log.V(1).Info("Starting the compliance events API and controller") - - k8sClient := kubernetes.NewForConfigOrDie(mgr.GetConfig()) - - tempDir, err := os.MkdirTemp("", "compliance-events-store") - if err != nil { - log.Error(err, "Failed to create a temporary directory") - os.Exit(1) - } - - defer func() { - err := os.RemoveAll(tempDir) - if err != nil { - log.Error(err, "Failed to clean up the temporary directory", "path", tempDir) - } - }() - - complianceEventsNamespace, _ := os.LookupEnv(complianceeventsapi.WatchNamespaceEnvVar) - if complianceEventsNamespace == "" { - namespace, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") - if err == nil { - complianceEventsNamespace = string(namespace) - } else { - log.Info("Could not detect the controller namespace. Assuming open-cluster-management.") - - complianceEventsNamespace = "open-cluster-management" - } - } + log.Info("Starting the template resolver service") wg := sync.WaitGroup{} - log.Info("Starting the compliance events API") - - complianceServerCtx := startComplianceEventsAPI( - controllerCtx, - cfg, - k8sClient, - clusterID, - complianceEventsNamespace, - net.JoinHostPort(complianceAPIHost, complianceAPIPort), - complianceAPICert, - complianceAPIKey, - &wg, - tempDir, - replicatedPolicyUpdates, - ) - - log.Info("Starting the template resolver service") - resolvers, saTemplatesSource := propagatorctrl.NewTemplateResolvers( controllerCtx, mgr.GetConfig(), mgr.GetClient(), templateResolver, replicatedPolicyUpdates, ) @@ -574,11 +477,10 @@ func main() { }() replicatedPolicyCtrler := &propagatorctrl.ReplicatedPolicyReconciler{ - Propagator: propagator, - ResourceVersions: replicatedResourceVersions, - DynamicWatcher: dynamicWatcher, - ComplianceServerCtx: complianceServerCtx, - TemplateResolvers: resolvers, + Propagator: propagator, + ResourceVersions: replicatedResourceVersions, + DynamicWatcher: dynamicWatcher, + TemplateResolvers: resolvers, } if err = replicatedPolicyCtrler.SetupWithManager(mgr, replPolicyMaxConcurrency, @@ -604,154 +506,6 @@ func main() { wg.Wait() } -func startComplianceEventsAPI( - ctx context.Context, - cfg *rest.Config, - client *kubernetes.Clientset, - clusterID string, - controllerNamespace string, - complianceAPIAddr string, - complianceAPICert string, - complianceAPIKey string, - wg *sync.WaitGroup, - tempDir string, - reconcileRequests chan<- event.GenericEvent, -) *complianceeventsapi.ComplianceServerCtx { - var dbConnectionURL string - - dbSecret, err := client.CoreV1().Secrets(controllerNamespace).Get( - ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, - ) - if k8serrors.IsNotFound(err) { - log.Info( - "Could not start the compliance events API. To enable this functionality, ensure the Postgres "+ - "connection secret is valid in the controller namespace.", - "secretName", complianceeventsapi.DBSecretName, - "namespace", controllerNamespace, - ) - } else if err != nil { - log.Error( - err, - "Failed to determine if the secret was defined", - "secretName", complianceeventsapi.DBSecretName, - "namespace", controllerNamespace, - ) - - os.Exit(1) - } else { - var err error - - dbConnectionURL, err = complianceeventsapi.ParseDBSecret(dbSecret, tempDir) - if err != nil { - log.Error( - err, - "Fix the connection details to enable the compliance events API feature", - "secret", complianceeventsapi.DBSecretName, - "namespace", controllerNamespace, - ) - } - } - - complianceServerCtx, err := complianceeventsapi.NewComplianceServerCtx(dbConnectionURL, clusterID) - if err == nil { - // If the migration failed, MigrateDB will log it and MonitorDatabaseConnection will fix it. - err := complianceServerCtx.MigrateDB(ctx, client, controllerNamespace) - if err != nil { - log.Info("Will periodically retry the migration until it is successful") - } - } else if !errors.Is(err, complianceeventsapi.ErrInvalidConnectionURL) { - log.Error(err, "Unexpected error") - - os.Exit(1) - } - - reconciler := complianceeventsapi.ComplianceDBSecretReconciler{ - Client: client, ComplianceServerCtx: complianceServerCtx, TempDir: tempDir, ConnectionURL: dbConnectionURL, - } - - dbSecretDynamicWatcher, err := k8sdepwatches.New( - cfg, &reconciler, &k8sdepwatches.Options{EnableCache: true}, - ) - if err != nil { - log.Error(err, "Failed to instantiate the dynamic watcher for the compliance events database secret reconciler") - os.Exit(1) - } - - reconciler.DynamicWatcher = dbSecretDynamicWatcher - - var cert *tls.Certificate - - if complianceAPICert != "" && complianceAPIKey != "" { - certTemp, err := tls.LoadX509KeyPair(complianceAPICert, complianceAPIKey) - if err != nil { - log.Error( - err, - "Failed to parse the provided TLS certificate and key", - "cert", complianceAPICert, - "key", complianceAPIKey, - ) - os.Exit(1) - } - - cert = &certTemp - } else { - log.Info("The compliance events history API will listen on HTTP since no certificate was provided") - } - - complianceAPI := complianceeventsapi.NewComplianceAPIServer(complianceAPIAddr, cfg, cert) - - wg.Add(1) - - go func() { - if err := complianceAPI.Start(ctx, complianceServerCtx); err != nil { - log.Error(err, "Failed to start the compliance API server") - - os.Exit(1) - } - - wg.Done() - }() - - wg.Add(1) - - go func() { - err := dbSecretDynamicWatcher.Start(ctx) - if err != nil { - log.Error( - err, - "Unable to start the compliance events database secret watcher", - "controller", complianceeventsapi.ControllerName, - ) - os.Exit(1) - } - - wg.Done() - }() - - <-dbSecretDynamicWatcher.Started() - - go complianceeventsapi.MonitorDatabaseConnection( - ctx, complianceServerCtx, client, controllerNamespace, reconcileRequests, - ) - - watcherSecret := k8sdepwatches.ObjectIdentifier{ - Version: "v1", - Kind: "Secret", - Namespace: controllerNamespace, - Name: complianceeventsapi.DBSecretName, - } - if err := dbSecretDynamicWatcher.AddWatcher(watcherSecret, watcherSecret); err != nil { - log.Error( - err, - "Unable to start the compliance events database secret watcher", - "controller", complianceeventsapi.ControllerName, - ) - os.Exit(1) - } - - return complianceServerCtx -} - // reportMetrics returns a bool on whether to report GRC metrics from the propagator func reportMetrics() bool { metrics, _ := os.LookupEnv("DISABLE_REPORT_METRICS") diff --git a/main_test.go b/main_test.go index 2a1cce4b..64a0d2d4 100644 --- a/main_test.go +++ b/main_test.go @@ -17,9 +17,6 @@ func TestRunMain(t *testing.T) { os.Args = append(os.Args, "--leader-elect=false", "--enable-webhooks=false", - "--compliance-history-api-port=8385", - "--compliance-history-api-cert=dev-tls.crt", - "--compliance-history-api-key=dev-tls.key", ) main() diff --git a/test/e2e/case14_root_policy_metrics_test.go b/test/e2e/case14_root_policy_metrics_test.go index 7246fb23..f75a2eb6 100644 --- a/test/e2e/case14_root_policy_metrics_test.go +++ b/test/e2e/case14_root_policy_metrics_test.go @@ -75,9 +75,6 @@ var _ = Describe("Test root policy metrics", Ordered, func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) diff --git a/test/e2e/case18_compliance_api_test.go b/test/e2e/case18_compliance_api_test.go deleted file mode 100644 index 434f65db..00000000 --- a/test/e2e/case18_compliance_api_test.go +++ /dev/null @@ -1,2474 +0,0 @@ -// Copyright Contributors to the Open Cluster Management project - -package e2e - -import ( - "bytes" - "context" - "crypto/tls" - "database/sql" - "encoding/csv" - "encoding/json" - "errors" - "fmt" - "io" - "math/rand" - "net/http" - "strings" - "time" - - "github.com/lib/pq" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - - "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" - "open-cluster-management.io/governance-policy-propagator/test/utils" -) - -const ( - eventsEndpoint = "http://localhost:8385/api/v1/compliance-events" - csvEndpoint = "http://localhost:8385/api/v1/reports/compliance-events" -) - -var httpClient = http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, -} - -var ( - wrongSAToken string - subsetSAToken string -) - -const ( - wrongSAYaml = "../resources/case18_compliance_api_test/wrong_service_account.yaml" - subsetSAYaml = "../resources/case18_compliance_api_test/subset_service_account.yaml" - policiesQueryByName = "SELECT policies.id, policies.kind, policies.api_group, policies.name, " + - "policies.namespace, policies.severity, specs.spec FROM policies " + - "LEFT JOIN specs ON policies.spec_id=specs.id WHERE name = $1 " -) - -func getTableNames(db *sql.DB) ([]string, error) { - tableNameRows, err := db.Query("SELECT tablename FROM pg_tables WHERE schemaname = current_schema()") - if err != nil { - return nil, err - } else if tableNameRows.Err() != nil { - return nil, err - } - - defer tableNameRows.Close() - - tableNames := []string{} - - for tableNameRows.Next() { - var tableName string - - err := tableNameRows.Scan(&tableName) - if err != nil { - return nil, err - } - - tableNames = append(tableNames, tableName) - } - - return tableNames, nil -} - -// Note: These tests require a running Postgres server running in the Kind cluster from the "postgres" Make target. -var _ = Describe("Test the compliance events API", Label("compliance-events-api"), Serial, Ordered, func() { - var k8sConfig *rest.Config - var k8sClient *kubernetes.Clientset - var db *sql.DB - - BeforeAll(func(ctx context.Context) { - var err error - - k8sConfig, err = LoadConfig("", "", "") - Expect(err).ToNot(HaveOccurred()) - - Expect(clientToken).ToNot(BeEmpty(), "Ensure you use the service account kubeconfig (kubeconfig_hub)") - - k8sClient, err = kubernetes.NewForConfig(k8sConfig) - Expect(err).ToNot(HaveOccurred()) - - connectionURL := "postgresql://grc:grc@localhost:5432/ocm-compliance-history?sslmode=disable" - db, err = sql.Open("postgres", connectionURL) - DeferCleanup(func() { - if db == nil { - return - } - - Expect(db.Close()).To(Succeed()) - }) - - Expect(err).ToNot(HaveOccurred()) - - Expect(db.PingContext(ctx)).To(Succeed()) - - // Drop all tables to start fresh - tableNameRows, err := db.Query("SELECT tablename FROM pg_tables WHERE schemaname = current_schema()") - Expect(err).ToNot(HaveOccurred()) - - defer tableNameRows.Close() - - tableNames, err := getTableNames(db) - Expect(err).ToNot(HaveOccurred()) - - for _, tableName := range tableNames { - _, err := db.ExecContext(ctx, "DROP TABLE IF EXISTS "+tableName+" CASCADE") - Expect(err).ToNot(HaveOccurred()) - } - - complianceServerCtx, err := complianceeventsapi.NewComplianceServerCtx(connectionURL, "unknown") - Expect(err).ToNot(HaveOccurred()) - - err = complianceServerCtx.MigrateDB(ctx, k8sClient, "open-cluster-management") - Expect(err).ToNot(HaveOccurred()) - - complianceAPI := complianceeventsapi.NewComplianceAPIServer("localhost:8385", k8sConfig, nil) - - httpCtx, httpCtxCancel := context.WithCancel(context.Background()) - - go func() { - defer GinkgoRecover() - - err = complianceAPI.Start(httpCtx, complianceServerCtx) - Expect(err).ToNot(HaveOccurred()) - }() - - DeferCleanup(func() { - httpCtxCancel() - }) - - Expect(err).ToNot(HaveOccurred()) - - By("Add a new wrong-service account") - utils.Kubectl("apply", "-f", wrongSAYaml, "--kubeconfig="+kubeconfigHub) - utils.Kubectl("apply", "-f", subsetSAYaml, "--kubeconfig="+kubeconfigHub) - - wrongSAToken = getToken(ctx, "default", "wrong-sa") - subsetSAToken = getToken(ctx, "default", "subset-sa") - }) - - Describe("Test the database migrations", func() { - It("Migrates from a clean database", func(ctx context.Context) { - tableNames, err := getTableNames(db) - Expect(err).ToNot(HaveOccurred()) - Expect(tableNames).To(ContainElements( - "clusters", "parent_policies", "policies", "compliance_events", "specs", - )) - - migrationVersionRows := db.QueryRow("SELECT version, dirty FROM schema_migrations") - var version int - var dirty bool - err = migrationVersionRows.Scan(&version, &dirty) - Expect(err).ToNot(HaveOccurred()) - Expect(version).To(Equal(2)) - Expect(dirty).To(BeFalse()) - }) - }) - - Describe("Test POSTing Events", func() { - Describe("POST one valid event with including all the optional fields", func() { - payload := []byte(`{ - "cluster": { - "name": "managed1", - "cluster_id": "test1-managed1-fake-uuid-1" - }, - "parent_policy": { - "name": "etcd-encryption1", - "namespace": "policies", - "categories": ["cat-1", "cat-2"], - "controls": ["ctrl-1"], - "standards": ["stand-1"] - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "etcd-encryption1", - "namespace": "local-cluster", - "spec": {"test": "one", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [etcd] not found in namespace default", - "timestamp": "2023-01-01T01:01:01.111Z", - "metadata": {"test": true}, - "reported_by": "optional-test" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the event") - Eventually(postEvent(ctx, payload, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should have created the cluster in a table", func() { - rows, err := db.Query("SELECT * FROM clusters WHERE cluster_id = $1", "test1-managed1-fake-uuid-1") - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - name string - clusterID string - ) - err := rows.Scan(&id, &name, &clusterID) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - Expect(name).To(Equal("managed1")) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have created the parent policy in a table", func() { - rows, err := db.Query( - "SELECT * FROM parent_policies WHERE name = $1 AND namespace= $2", "etcd-encryption1", "policies", - ) - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - name string - namespace string - cats pq.StringArray - ctrls pq.StringArray - stands pq.StringArray - ) - - err := rows.Scan(&id, &name, &namespace, &cats, &ctrls, &stands) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - Expect(cats).To(ContainElements("cat-1", "cat-2")) - Expect(ctrls).To(ContainElements("ctrl-1")) - Expect(stands).To(ContainElements("stand-1")) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have created the policy in a table", func() { - rows, err := db.Query(policiesQueryByName, "etcd-encryption1") - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - kind string - apiGroup string - name string - ns *string - severity *string - spec complianceeventsapi.JSONMap - ) - - err := rows.Scan(&id, &kind, &apiGroup, &name, &ns, &severity, &spec) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - Expect(kind).To(Equal("ConfigurationPolicy")) - Expect(apiGroup).To(Equal("policy.open-cluster-management.io")) - Expect(ns).ToNot(BeNil()) - Expect(*ns).To(Equal("local-cluster")) - Expect(spec).ToNot(BeNil()) - Expect(spec).To(BeEquivalentTo(map[string]any{"test": "one", "severity": "low"})) - Expect(severity).ToNot(BeNil()) - Expect(*severity).To(Equal("low")) - - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have created the event in a table", func() { - rows, err := db.Query("SELECT * FROM compliance_events WHERE timestamp = $1", - "2023-01-01T01:01:01.111Z") - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - clusterID int - policyID int - parentPolicyID *int - compliance string - message string - timestamp string - metadata complianceeventsapi.JSONMap - reportedBy *string - messageHash string - ) - - err := rows.Scan(&id, &clusterID, &policyID, &parentPolicyID, &compliance, &message, ×tamp, - &metadata, &reportedBy, &messageHash) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).To(Equal(1)) - Expect(clusterID).To(Equal(1)) - Expect(policyID).To(Equal(1)) - Expect(parentPolicyID).NotTo(BeNil()) - Expect(*parentPolicyID).To(Equal(1)) - Expect(compliance).To(Equal("NonCompliant")) - Expect(message).To(Equal("configmaps [etcd] not found in namespace default")) - Expect(timestamp).To(Equal("2023-01-01T01:01:01.111Z")) - Expect(metadata).To(HaveKeyWithValue("test", true)) - Expect(messageHash).To(Equal("3feb697b1df4585ef4ac1623ca233a34b2a2ea84")) - Expect(reportedBy).ToNot(BeNil()) - Expect(*reportedBy).To(Equal("optional-test")) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should return the compliance event from the API", func(ctx context.Context) { - respJSON, err := listEvents(ctx, clientToken) - Expect(err).ToNot(HaveOccurred()) - - complianceEvent := map[string]any{ - "cluster": map[string]any{ - "cluster_id": "test1-managed1-fake-uuid-1", - "name": "managed1", - }, - "event": map[string]any{ - "compliance": "NonCompliant", - "message": "configmaps [etcd] not found in namespace default", - "metadata": map[string]any{"test": true}, - "reported_by": "optional-test", - "timestamp": "2023-01-01T01:01:01.111Z", - }, - "id": float64(1), - "parent_policy": map[string]any{ - "categories": []any{"cat-1", "cat-2"}, - "controls": []any{"ctrl-1"}, - "id": float64(1), - "name": "etcd-encryption1", - "namespace": "policies", - "standards": []any{"stand-1"}, - }, - "policy": map[string]any{ - "apiGroup": "policy.open-cluster-management.io", - "id": float64(1), - "kind": "ConfigurationPolicy", - "name": "etcd-encryption1", - "namespace": "local-cluster", - "severity": "low", - }, - } - - expected := map[string]any{ - "data": []any{complianceEvent}, - "metadata": map[string]any{ - "page": float64(1), - "pages": float64(1), - "per_page": float64(20), - "total": float64(1), - }, - } - - Expect(respJSON).To(Equal(expected)) - - // Get just the single compliance event - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint+"/1", nil) - Expect(err).ToNot(HaveOccurred()) - - // Set auth token - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - - respJSON = map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ToNot(HaveOccurred()) - - complianceEvent["policy"].(map[string]any)["spec"] = map[string]any{ - "severity": "low", - "test": "one", - } - - Expect(respJSON).To(Equal(complianceEvent)) - }) - - It("Should return the compliance event with the spec from the API", func(ctx context.Context) { - respJSON, err := listEvents(ctx, clientToken, "include_spec") - Expect(err).ToNot(HaveOccurred()) - - data := respJSON["data"].([]any) - Expect(data).To(HaveLen(1)) - - spec := data[0].(map[string]any)["policy"].(map[string]any)["spec"] - expected := map[string]any{"test": "one", "severity": "low"} - - Expect(spec).To(Equal(expected)) - }) - }) - - Describe("POST two minimally-valid events on different clusters and policies", func() { - payload1 := []byte(`{ - "cluster": { - "name": "managed2", - "cluster_id": "test2-managed2-fake-uuid-2" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "etcd-encryption2", - "spec": {"test": "two"} - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [etcd] not found in namespace default", - "timestamp": "2023-02-02T02:02:02.222Z" - } - }`) - - payload2 := []byte(`{ - "cluster": { - "name": "managed3", - "cluster_id": "test2-managed3-fake-uuid-3" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "etcd-encryption2", - "spec": {"different-spec-test": "two-and-a-half"} - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [etcd] found in namespace default", - "timestamp": "2023-02-02T02:02:02.223Z" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the events") - Eventually(postEvent(ctx, payload1, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload2, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should have created both clusters in a table", func() { - rows, err := db.Query("SELECT * FROM clusters") - Expect(err).ToNot(HaveOccurred()) - - clusternames := make([]string, 0) - - for rows.Next() { - var ( - id int - name string - clusterID string - ) - err := rows.Scan(&id, &name, &clusterID) - Expect(err).ToNot(HaveOccurred()) - - clusternames = append(clusternames, name) - } - - Expect(clusternames).To(ContainElements("managed2", "managed3")) - }) - - It("Should have created two policies in a table despite having the same name", func() { - rows, err := db.Query(policiesQueryByName, "etcd-encryption2") - Expect(err).ToNot(HaveOccurred()) - - rowCount := 0 - - for rows.Next() { - var ( - id int - kind string - apiGroup string - name string - ns *string - severity *string - spec complianceeventsapi.JSONMap - ) - - err := rows.Scan(&id, &kind, &apiGroup, &name, &ns, &severity, &spec) - Expect(err).ToNot(HaveOccurred()) - - rowCount++ - Expect(id).To(Equal(1 + rowCount)) - } - - Expect(rowCount).To(Equal(2)) - }) - - It("Should have created both events in a table", func() { - rows, err := db.Query("SELECT * FROM compliance_events WHERE timestamp > $1", - "2023-02-02T02:02:02.221Z") - Expect(err).ToNot(HaveOccurred()) - - messages := make([]string, 0) - for rows.Next() { - var ( - id int - clusterID int - policyID int - parentPolicyID *int - compliance string - message string - timestamp string - metadata *string - reportedBy *string - messageHash string - ) - - err := rows.Scan(&id, &clusterID, &policyID, &parentPolicyID, &compliance, &message, ×tamp, - &metadata, &reportedBy, &messageHash) - Expect(err).ToNot(HaveOccurred()) - - messages = append(messages, message) - - Expect(id).NotTo(Equal(0)) - Expect(clusterID).NotTo(Equal(0)) - Expect(policyID).To(Equal(1 + len(messages))) - Expect(parentPolicyID).To(BeNil()) - } - - Expect(messages).To(ConsistOf( - "configmaps [etcd] found in namespace default", - "configmaps [etcd] not found in namespace default", - )) - }) - }) - - Describe("API pagination", func() { - It("Should have correct default pagination", func(ctx context.Context) { - respJSON, err := listEvents(ctx, clientToken) - Expect(err).ToNot(HaveOccurred()) - - metadata := respJSON["metadata"].(map[string]interface{}) - Expect(metadata["page"]).To(BeEquivalentTo(1)) - Expect(metadata["pages"]).To(BeEquivalentTo(1)) - Expect(metadata["per_page"]).To(BeEquivalentTo(20)) - Expect(metadata["total"]).To(BeEquivalentTo(3)) - - data := respJSON["data"].([]any) - Expect(data).To(HaveLen(3)) - }) - It("Should have accept page=2", func(ctx context.Context) { - respJSON, err := listEvents(ctx, clientToken, "page=2") - Expect(err).ToNot(HaveOccurred()) - - metadata := respJSON["metadata"].(map[string]interface{}) - Expect(metadata["page"]).To(BeEquivalentTo(2)) - Expect(metadata["pages"]).To(BeEquivalentTo(1)) - Expect(metadata["per_page"]).To(BeEquivalentTo(20)) - Expect(metadata["total"]).To(BeEquivalentTo(3)) - - data := respJSON["data"].([]any) - Expect(data).To(BeEmpty()) - }) - - It("Should accept per_page=2 and page=2", func(ctx context.Context) { - respJSON, err := listEvents(ctx, clientToken, "per_page=2", "page=2") - Expect(err).ToNot(HaveOccurred()) - - metadata := respJSON["metadata"].(map[string]interface{}) - Expect(metadata["page"]).To(BeEquivalentTo(2)) - Expect(metadata["pages"]).To(BeEquivalentTo(2)) - Expect(metadata["per_page"]).To(BeEquivalentTo(2)) - Expect(metadata["total"]).To(BeEquivalentTo(3)) - - data := respJSON["data"].([]any) - Expect(data).To(HaveLen(1)) - // The default sort is descending order by event timestamp, so the last event in the pagination is - // the first event. - Expect(data[0].(map[string]any)["id"]).To(BeEquivalentTo(1)) - }) - - It("Should not accept page=150", func(ctx context.Context) { - // Too many per_page - _, err := listEvents(ctx, clientToken, "per_page=150", "page=2") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("per_page must be a value between 1 and 100"))) - }) - - It("Should not accept per_page=-5", func(ctx context.Context) { - // Too little per_page - _, err := listEvents(ctx, clientToken, "per_page=-5", "page=2") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("per_page must be a value between 1 and 100"))) - }) - - It("Should not accept page=-5", func(ctx context.Context) { - // Too little per_page - _, err := listEvents(ctx, clientToken, "page=-5") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("page must be a positive integer"))) - }) - }) - - DescribeTable("API sorting", - func(ctx context.Context, queryArgs []string, expectedIDs []float64) { - respJSON, err := listEvents(ctx, clientToken, queryArgs...) - Expect(err).ToNot(HaveOccurred()) - - data, ok := respJSON["data"].([]any) - Expect(ok).To(BeTrue()) - Expect(data).To(HaveLen(3)) - - actualIDs := make([]float64, 0, 3) - - for _, event := range data { - actualIDs = append(actualIDs, event.(map[string]any)["id"].(float64)) - } - - Expect(actualIDs).To(Equal(expectedIDs)) - }, - Entry( - "Sort descending by cluster.cluster_id", - []string{"sort=cluster.cluster_id", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by cluster.cluster_id", - []string{"sort=cluster.cluster_id", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by cluster.name", - []string{"sort=cluster.name", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by cluster.name", - []string{"sort=cluster.name", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by event.compliance", - []string{"sort=event.compliance", "direction=desc"}, - []float64{2, 1, 3}, - ), - Entry( - "Sort ascending by event.compliance", - []string{"sort=event.compliance", "direction=asc"}, - []float64{3, 1, 2}, - ), - Entry( - "Sort descending by event.message", - []string{"sort=event.message", "direction=desc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort ascending by event.message", - []string{"sort=event.message", "direction=asc"}, - []float64{3, 1, 2}, - ), - Entry( - "Sort descending by event.reported_by", - []string{"sort=event.reported_by", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by event.reported_by", - []string{"sort=event.reported_by", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by event.timestamp (default)", - []string{}, - []float64{3, 2, 1}, - ), - Entry( - "Sort descending by event.timestamp", - []string{"sort=event.timestamp", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by event.timestamp", - []string{"sort=event.timestamp", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.categories", - []string{"sort=parent_policy.categories", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.categories", - []string{"sort=parent_policy.categories", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.controls", - []string{"sort=parent_policy.controls", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.controls", - []string{"sort=parent_policy.controls", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.id", - []string{"sort=parent_policy.id", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.id", - []string{"sort=parent_policy.id", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.name", - []string{"sort=parent_policy.name", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.name", - []string{"sort=parent_policy.name", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.namespace", - []string{"sort=parent_policy.namespace", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.namespace", - []string{"sort=parent_policy.namespace", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.standards", - []string{"sort=parent_policy.standards", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by parent_policy.standards", - []string{"sort=parent_policy.standards", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.apiGroup", - []string{"sort=policy.apiGroup", "direction=desc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort ascending by policy.apiGroup", - []string{"sort=policy.apiGroup", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.id", - []string{"sort=policy.id", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by policy.id", - []string{"sort=policy.id", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.kind", - []string{"sort=policy.kind", "direction=desc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort ascending by policy.kind", - []string{"sort=policy.kind", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.name", - []string{"sort=policy.name", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by policy.name", - []string{"sort=policy.name", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.namespace", - []string{"sort=policy.namespace", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by policy.namespace", - []string{"sort=policy.namespace", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by policy.severity", - []string{"sort=policy.severity", "direction=desc"}, - []float64{2, 3, 1}, - ), - Entry( - "Sort ascending by policy.severity", - []string{"sort=policy.severity", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by parent_policy.id and policy.id", - []string{"sort=parent_policy.id,policy.id", "direction=asc"}, - []float64{1, 2, 3}, - ), - Entry( - "Sort descending by id", - []string{"sort=id", "direction=desc"}, - []float64{3, 2, 1}, - ), - Entry( - "Sort ascending by id", - []string{"sort=id", "direction=asc"}, - []float64{1, 2, 3}, - ), - ) - - Describe("Invalid event ID", func() { - It("Compliance event is not found", func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint+"/1231291", nil) - Expect(err).ToNot(HaveOccurred()) - - // Set auth token - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - defer resp.Body.Close() - - Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) - - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ToNot(HaveOccurred()) - - Expect(respJSON["message"].(string)).To(Equal("The requested compliance event was not found")) - }) - - It("Compliance event ID is invalid", func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint+"/sql-injections-lose", nil) - Expect(err).ToNot(HaveOccurred()) - - // Set auth token - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - defer resp.Body.Close() - - Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) - - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ToNot(HaveOccurred()) - - Expect(respJSON["message"].(string)).To(Equal("The provided compliance event ID is invalid")) - }) - }) - - Describe("Invalid sort options", func() { - It("An invalid sort of sort=my-laundry", func(ctx context.Context) { - _, err := listEvents(ctx, clientToken, "sort=my-laundry") - Expect(err).To(HaveOccurred()) - expected := "an invalid sort option was provided, choose from: cluster.cluster_id, cluster.name, " + - "event.compliance, event.message, event.reported_by, event.timestamp, id, " + - "parent_policy.categories, parent_policy.controls, parent_policy.id, parent_policy.name, " + - "parent_policy.namespace, parent_policy.standards, policy.apiGroup, policy.id, policy.kind, " + - "policy.name, policy.namespace, policy.severity" - Expect(err).To(MatchError(ContainSubstring(expected))) - }) - - It("An invalid sort direction", func(ctx context.Context) { - _, err := listEvents(ctx, clientToken, "direction=up") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("direction must be one of: asc, desc"))) - }) - }) - - Describe("Invalid query arguments", func() { - It("An invalid query argument", func(ctx context.Context) { - _, err := listEvents(ctx, clientToken, "make_it_compliant=please") - expected := "an invalid query argument was provided, choose from: cluster.cluster_id, cluster.name, " + - "direction, event.compliance, event.message, event.message_includes, event.message_like, " + - "event.reported_by, event.timestamp, event.timestamp_after, event.timestamp_before, id, " + - "include_spec, page, parent_policy.categories, parent_policy.controls, parent_policy.id, " + - "parent_policy.name, parent_policy.namespace, parent_policy.standards, per_page, " + - "policy.apiGroup, policy.id, policy.kind, policy.name, policy.namespace, policy.severity, sort" - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring(expected))) - }) - - It("An invalid include_spec=yes-please", func(ctx context.Context) { - _, err := listEvents(ctx, clientToken, "include_spec=yes-please") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("include_spec is a flag and does not accept a value"))) - }) - - It("An invalid sort direction", func(ctx context.Context) { - _, err := listEvents(ctx, clientToken, "direction=up") - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("direction must be one of: asc, desc"))) - }) - }) - - Describe("POST three events on the same cluster and policy", func() { - // payload1 defines most things, and should cause the cluster, parent, and policy to be created. - payload1 := []byte(`{ - "cluster": { - "name": "managed4", - "cluster_id": "test3-managed4-fake-uuid-4" - }, - "parent_policy": { - "name": "common-parent", - "namespace": "policies", - "categories": ["cat-3", "cat-4"], - "controls": ["ctrl-2"], - "standards": ["stand-2"] - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common", - "spec": {"test": "three", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [common] not found in namespace default", - "timestamp": "2023-03-03T03:03:03.333Z" - } - }`) - - // payload2 just uses the ids for the policy and parent_policy. - payload2 := []byte(`{ - "cluster": { - "name": "managed4", - "cluster_id": "test3-managed4-fake-uuid-4" - }, - "parent_policy": { - "id": 2 - }, - "policy": { - "id": 4 - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [common] not found in namespace default", - "timestamp": "2023-04-04T04:04:04.444Z" - } - }`) - - // payload3 redefines most things, and should cause the cluster, parent, and policy to be reused from the - // cache. - payload3 := []byte(`{ - "cluster": { - "name": "managed4", - "cluster_id": "test3-managed4-fake-uuid-4" - }, - "parent_policy": { - "name": "common-parent", - "namespace": "policies", - "categories": ["cat-3", "cat-4"], - "controls": ["ctrl-2"], - "standards": ["stand-2"] - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common", - "spec": {"test": "three", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [common] not found in namespace default", - "timestamp": "2023-05-05T05:05:05.555Z" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the events") - Eventually(postEvent(ctx, payload1, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload2, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload3, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should have only created one cluster in the table", func() { - rows, err := db.Query("SELECT * FROM clusters WHERE name = $1", "managed4") - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - name string - clusterID string - ) - err := rows.Scan(&id, &name, &clusterID) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have only created one parent policy in a table", func() { - rows, err := db.Query( - "SELECT * FROM parent_policies WHERE name = $1 AND namespace = $2", "common-parent", "policies", - ) - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - name string - namespace string - cats pq.StringArray - ctrls pq.StringArray - stands pq.StringArray - ) - - err := rows.Scan(&id, &name, &namespace, &cats, &ctrls, &stands) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have only created one policy in a table", func() { - rows, err := db.Query(policiesQueryByName, "common") - Expect(err).ToNot(HaveOccurred()) - - specs := make([]complianceeventsapi.JSONMap, 0, 1) - for rows.Next() { - var ( - id int - kind string - apiGroup string - name string - ns *string - severity *string - spec complianceeventsapi.JSONMap - ) - - err := rows.Scan(&id, &kind, &apiGroup, &name, &ns, &severity, &spec) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - specs = append(specs, spec) - } - - Expect(specs).To(HaveLen(1)) - Expect(specs[0]).To(BeEquivalentTo(map[string]any{"test": "three", "severity": "low"})) - }) - - It("Should have created three events in a table", func() { - rows, err := db.Query("SELECT * FROM compliance_events WHERE message = $1", - "configmaps [common] not found in namespace default") - Expect(err).ToNot(HaveOccurred()) - - timestamps := make([]string, 0, 3) - for rows.Next() { - var ( - id int - clusterID int - policyID int - parentPolicyID *int - compliance string - message string - timestamp string - metadata *string - reportedBy *string - messageHash string - ) - - err := rows.Scan(&id, &clusterID, &policyID, &parentPolicyID, &compliance, &message, ×tamp, - &metadata, &reportedBy, &messageHash) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - Expect(clusterID).NotTo(Equal(0)) - Expect(policyID).To(Equal(4)) - Expect(parentPolicyID).NotTo(BeNil()) - Expect(*parentPolicyID).To(Equal(2)) - - timestamps = append(timestamps, timestamp) - } - - Expect(timestamps).To(ConsistOf( - "2023-03-03T03:03:03.333Z", - "2023-04-04T04:04:04.444Z", - "2023-05-05T05:05:05.555Z", - )) - }) - }) - - Describe("POST events to check parent policy matching", func() { - // payload1 defines most things, and should cause the cluster, parent, and policy to be created. - payload1 := []byte(`{ - "cluster": { - "name": "managed5", - "cluster_id": "test5-managed5-fake-uuid-5" - }, - "parent_policy": { - "name": "parent-a", - "namespace": "policies", - "standards": ["stand-3"] - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common-a", - "spec": {"test": "four", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [common] found in namespace default", - "timestamp": "2023-05-05T05:05:05.555Z" - } - }`) - - // payload2 skips the standards array on the parent policy, - // which should create a new parent policy - payload2 := []byte(`{ - "cluster": { - "name": "managed5", - "cluster_id": "test5-managed5-fake-uuid-5" - }, - "parent_policy": { - "name": "parent-a", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common-a", - "spec": {"test": "four", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [common] found in namespace default", - "timestamp": "2023-06-06T06:06:06.666Z" - } - }`) - - // payload3 defines the standards with an empty array, - // which should be the same as not specifying it at all (payload2) - payload3 := []byte(`{ - "cluster": { - "name": "managed5", - "cluster_id": "test5-managed5-fake-uuid-5" - }, - "parent_policy": { - "name": "parent-a", - "namespace": "policies", - "standards": [] - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common-a", - "spec": {"test": "four", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [common] found in namespace default", - "timestamp": "2023-07-07T07:07:07.777Z" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the events") - Eventually(postEvent(ctx, payload1, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload2, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload3, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should have created two parent policies", func() { - rows, err := db.Query( - "SELECT * FROM parent_policies WHERE name = $1 AND namespace = $2", "parent-a", "policies", - ) - Expect(err).ToNot(HaveOccurred()) - - standardArrays := make([]pq.StringArray, 0) - for rows.Next() { - var ( - id int - name string - namespace string - cats pq.StringArray - ctrls pq.StringArray - stands pq.StringArray - ) - - err := rows.Scan(&id, &name, &namespace, &cats, &ctrls, &stands) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - standardArrays = append(standardArrays, stands) - } - - Expect(standardArrays).To(ConsistOf( - pq.StringArray{"stand-3"}, - nil, - )) - }) - - It("Should have created a single policy", func() { - rows, err := db.Query(policiesQueryByName, "common-a") - Expect(err).ToNot(HaveOccurred()) - - ids := make([]int, 0) - for rows.Next() { - var ( - id int - kind string - apiGroup string - name string - ns *string - severity *string - spec complianceeventsapi.JSONMap - ) - - err := rows.Scan(&id, &kind, &apiGroup, &name, &ns, &severity, &spec) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - ids = append(ids, id) - } - - Expect(ids).To(HaveLen(1)) - }) - }) - - Describe("POST events to check policy namespace matching", func() { - // payload1 should cause the cluster, parent, and policy to be created. - payload1 := []byte(`{ - "cluster": { - "name": "managed6", - "cluster_id": "test6-managed6-fake-uuid-6" - }, - "parent_policy": { - "name": "parent-b", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common-b", - "spec": {"test": "four", "severity": "low"}, - "severity": "low", - "namespace": "default" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [common] found in namespace default", - "timestamp": "2023-01-02T03:04:05.111Z" - } - }`) - - // payload2 skips the namespace, which should create a new policy - payload2 := []byte(`{ - "cluster": { - "name": "managed6", - "cluster_id": "test6-managed6-fake-uuid-6" - }, - "parent_policy": { - "name": "parent-b", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "common-b", - "spec": {"test": "four", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [common] found in namespace default", - "timestamp": "2023-01-02T03:04:05.222Z" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the events") - Eventually(postEvent(ctx, payload1, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - Eventually(postEvent(ctx, payload2, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should have created one parent policy", func() { - rows, err := db.Query( - "SELECT * FROM parent_policies WHERE name = $1 AND namespace = $2", "parent-b", "policies", - ) - Expect(err).ToNot(HaveOccurred()) - - count := 0 - for rows.Next() { - var ( - id int - name string - namespace string - cats pq.StringArray - ctrls pq.StringArray - stands pq.StringArray - ) - - err := rows.Scan(&id, &name, &namespace, &cats, &ctrls, &stands) - Expect(err).ToNot(HaveOccurred()) - Expect(id).NotTo(Equal(0)) - count++ - } - - Expect(count).To(Equal(1)) - }) - - It("Should have created two policies in the table, with different namespaces", func() { - rows, err := db.Query(policiesQueryByName, "common-b") - Expect(err).ToNot(HaveOccurred()) - - ids := make([]int, 0) - names := make([]string, 0) - namespaces := make([]string, 0) - specs := make([]complianceeventsapi.JSONMap, 0, 2) - for rows.Next() { - var ( - id int - kind string - apiGroup string - name string - ns *string - severity *string - spec complianceeventsapi.JSONMap - ) - - err := rows.Scan(&id, &kind, &apiGroup, &name, &ns, &severity, &spec) - Expect(err).ToNot(HaveOccurred()) - - Expect(id).NotTo(Equal(0)) - ids = append(ids, id) - names = append(names, name) - specs = append(specs, spec) - - if ns != nil { - namespaces = append(namespaces, *ns) - } - } - - Expect(ids).To(HaveLen(2)) - Expect(ids[0]).ToNot(Equal(ids[1])) - Expect(names[0]).To(Equal(names[1])) - Expect(namespaces).To(ConsistOf("default")) - Expect(specs[0]).To(Equal(specs[1])) - }) - }) - - Describe("POST invalid events", func() { - It("should require the cluster to be specified", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "parent_policy": { - "name": "validity-parent", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "validity", - "spec": {"test":"validity", "severity": "low"} - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the parent policy namespace to be specified", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": { - "name": "validity-parent" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "validity", - "spec": {"test":"validity", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the event time to be specified", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": { - "name": "validity-parent", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "validity", - "spec": {"test": "validity", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the parent policy to have fields when specified", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": {}, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "validity", - "spec": {"test": "validity", "severity": "low"}, - "severity": "low" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the policy to be defined", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": { - "name": "validity-parent", - "namespace": "policies" - }, - "policy": {}, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the input to be valid JSON", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - foo: bar: baz - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": { - "name": "validity-parent", - "namespace": "policies" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "validity", - "spec": {"test": "validity", "severity": "low"}, - "severity": "low", - "specHash": "foobar" - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should( - MatchError(ContainSubstring("Got non-201 status code 400")), - ) - }) - - It("should require the spec when inputting a new policy", func(ctx context.Context) { - Eventually(postEvent(ctx, []byte(`{ - "cluster": { - "name": "validity-test", - "cluster_id": "test-validity-fake-uuid" - }, - "parent_policy": { - "id": 1231234 - }, - "policy": { - "id": 123123 - }, - "event": { - "compliance": "Compliant", - "message": "configmaps [valid] valid in namespace valid", - "timestamp": "2023-09-09T09:09:09.999Z" - } - }`), clientToken), "5s", "1s").Should(MatchError(ContainSubstring( - `invalid input: parent_policy.id not found\\ninvalid input: policy.id not found`, - ))) - }) - }) - - DescribeTable("API filtering", - func(ctx context.Context, queryArgs []string, expectedIDs []float64) { - respJSON, err := listEvents(ctx, clientToken, queryArgs...) - Expect(err).ToNot(HaveOccurred()) - - data, ok := respJSON["data"].([]any) - Expect(ok).To(BeTrue()) - - actualIDs := []float64{} - - for _, event := range data { - actualIDs = append(actualIDs, event.(map[string]any)["id"].(float64)) - } - - Expect(actualIDs).To(Equal(expectedIDs)) - }, - Entry( - "Filter by cluster.cluster_id", - []string{"cluster.cluster_id=test1-managed1-fake-uuid-1,test6-managed6-fake-uuid-6"}, - []float64{11, 10, 1}, - ), - Entry( - "Filter by cluster.name", - []string{"cluster.name=managed1,managed6"}, - []float64{11, 10, 1}, - ), - Entry( - "Filter by event.compliance", - []string{"event.compliance=Compliant"}, - []float64{9, 8, 7, 3, 11, 10}, - ), - Entry( - "Filter by event.message", - []string{"event.message=configmaps%20%5Bcommon%5D%20not%20found%20in%20namespace%20default"}, - []float64{6, 5, 4}, - ), - Entry( - "Filter by event.message_includes", - []string{"event.message_includes=etcd"}, - []float64{3, 2, 1}, - ), - Entry( - "Filter by event.message_includes and ensure special characters are escaped", - []string{"event.message_includes=co_m%25n"}, - []float64{}, - ), - Entry( - "Filter by event.message_like", - []string{"event.message_like=configmaps%20%5B%25common%25%5D%25"}, - []float64{9, 8, 6, 7, 5, 4, 11, 10}, - ), - Entry( - "Filter by event.timestamp", - []string{"event.timestamp=2023-01-01T01:01:01.111Z"}, - []float64{1}, - ), - Entry( - "Filter by event.timestamp_after", - []string{"event.timestamp_after=2023-04-01T01:01:01.111Z"}, - []float64{9, 8, 7, 6, 5}, - ), - Entry( - "Filter by event.timestamp_before", - []string{"event.timestamp_before=2023-04-01T01:01:01.111Z"}, - []float64{4, 3, 2, 11, 10, 1}, - ), - Entry( - "Filter by event.timestamp_after and event.timestamp_before", - []string{ - "event.timestamp_after=2023-01-01T01:01:01.111Z", "event.timestamp_before=2023-04-01T01:01:01.111Z", - }, - []float64{4, 3, 2, 11, 10}, - ), - Entry( - "Filter by parent_policy.categories", - []string{"parent_policy.categories=cat-1,cat-3"}, - []float64{6, 5, 4, 1}, - ), - Entry( - "Filter by parent_policy.categories is null", - []string{"parent_policy.categories"}, - []float64{9, 8, 7, 3, 2, 11, 10}, - ), - Entry( - "Filter by parent_policy.controls", - []string{"parent_policy.controls=ctrl-2"}, - []float64{6, 5, 4}, - ), - Entry( - "Filter by parent_policy.controls is null", - []string{"parent_policy.controls"}, - []float64{9, 8, 7, 3, 2, 11, 10}, - ), - Entry( - "Filter by parent_policy.id", - []string{"parent_policy.id=2"}, - []float64{6, 5, 4}, - ), - Entry( - "Filter by parent_policy.name", - []string{"parent_policy.name=etcd-encryption1"}, - []float64{1}, - ), - Entry( - "Filter by parent_policy.namespace", - []string{"parent_policy.namespace=policies"}, - []float64{9, 8, 6, 7, 5, 4, 11, 10, 1}, - ), - Entry( - "Filter by parent_policy.standards", - []string{"parent_policy.standards=stand-2"}, - []float64{6, 5, 4}, - ), - Entry( - "Filter by parent_policy.standards is null", - []string{"parent_policy.standards"}, - []float64{9, 8, 3, 2, 11, 10}, - ), - Entry( - "Filter by policy.apiGroup", - []string{"policy.apiGroup=policy.open-cluster-management.io"}, - []float64{9, 8, 6, 7, 5, 4, 3, 2, 11, 10, 1}, - ), - Entry( - "Filter by policy.apiGroup no results", - []string{"policy.apiGroup=does-not-exist"}, - []float64{}, - ), - Entry( - "Filter by policy.id", - []string{"policy.id=4"}, - []float64{6, 5, 4}, - ), - Entry( - "Filter by policy.kind", - []string{"policy.kind=ConfigurationPolicy"}, - []float64{9, 8, 6, 7, 5, 4, 3, 2, 11, 10, 1}, - ), - Entry( - "Filter by policy.kind no results", - []string{"policy.kind=something-else"}, - []float64{}, - ), - Entry( - "Filter by policy.name", - []string{"policy.name=common-b"}, - []float64{11, 10}, - ), - Entry( - "Filter by policy.namespace", - []string{"policy.namespace=default"}, - []float64{10}, - ), - Entry( - "Filter by policy.namespace is null", - []string{"policy.namespace"}, - []float64{9, 8, 6, 7, 5, 4, 3, 2, 11}, - ), - Entry( - "Filter by policy.severity", - []string{"policy.severity=low"}, - []float64{9, 8, 6, 7, 5, 4, 11, 10, 1}, - ), - Entry( - "Filter by policy.severity is null", - []string{"policy.severity"}, - []float64{3, 2}, - ), - ) - - DescribeTable("Invalid API filtering", - func(ctx context.Context, queryArgs []string, expectedErrMsg string) { - _, err := listEvents(ctx, clientToken, queryArgs...) - Expect(err).To(MatchError(ContainSubstring(expectedErrMsg))) - }, - Entry( - "Filter by empty event.timestamp_before is invalid", - []string{"event.timestamp_before"}, - "invalid query argument: event.timestamp_before must have a value", - ), - Entry( - "Filter by invalid event.timestamp_before", - []string{"event.timestamp_before=1993"}, - "invalid query argument: event.timestamp_before must be in the format of RFC 3339", - ), - Entry( - "Filter by invalid event.timestamp_after", - []string{"event.timestamp_after=1993"}, - "invalid query argument: event.timestamp_after must be in the format of RFC 3339", - ), - ) - - Describe("Test the /api/v1/reports/compliance-events endpoint", func() { - It("should send CSV file in http response", func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, csvEndpoint, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - By("Content-type should be CSV") - Expect(resp.Header.Get("Content-Type")).Should(Equal("text/csv")) - Expect(resp.TransferEncoding).Should(ContainElement("chunked")) - - csvReader := csv.NewReader(resp.Body) - - records, err := csvReader.ReadAll() - Expect(err).ShouldNot(HaveOccurred()) - - Expect(len(records)).Should(BeNumerically(">", 10)) - - By("First line should be the titles") - Expect(records[0]).Should(ContainElements([]string{ - "compliance_events_id", - "compliance_events_compliance", - "compliance_events_message", - "compliance_events_metadata", - "compliance_events_reported_by", - "compliance_events_timestamp", - "clusters_cluster_id", - "clusters_name", - "parent_policies_id", - "parent_policies_name", - "parent_policies_namespace", - "parent_policies_categories", - "parent_policies_controls", - "parent_policies_standards", - "policies_id", - "policies_api_group", - "policies_kind", - "policies_name", - "policies_namespace", - "policies_severity", - })) - - By("All line should have 20 columns") - for _, r := range records { - Expect(r).Should(HaveLen(20)) - } - }) - It("Should return only header when SA does not have any GET verb to managedCluster", - func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, csvEndpoint, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Content-Type", "application/json") - // Set auth token - req.Header.Set("Authorization", "Bearer "+wrongSAToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - By("Content-type should be CSV") - Expect(resp.Header.Get("Content-Type")).Should(Equal("text/csv")) - - csvReader := csv.NewReader(resp.Body) - - records, err := csvReader.ReadAll() - Expect(err).ShouldNot(HaveOccurred()) - - By("Should return only header") - Expect(records).Should(HaveLen(1)) - - Expect(records[0]).Should(ContainElements([]string{ - "compliance_events_id", - "compliance_events_compliance", - "compliance_events_message", - "compliance_events_metadata", - "compliance_events_reported_by", - "compliance_events_timestamp", - "clusters_cluster_id", - "clusters_name", - "parent_policies_id", - "parent_policies_name", - "parent_policies_namespace", - "parent_policies_categories", - "parent_policies_controls", - "parent_policies_standards", - "policies_id", - "policies_api_group", - "policies_kind", - "policies_name", - "policies_namespace", - "policies_severity", - })) - }) - - DescribeTable("Should filter CSV file", - func(ctx context.Context, queryArgs []string, expectedLine int) { - endpoints := csvEndpoint - - endpoints += "?" + strings.Join(queryArgs, "&") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoints, nil) - Expect(err).ShouldNot(HaveOccurred()) - - // Set auth token - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - csvReader := csv.NewReader(resp.Body) - records, err := csvReader.ReadAll() - Expect(err).ShouldNot(HaveOccurred()) - - // The first element is title - Expect(records).Should(HaveLen(expectedLine)) - }, - Entry( - "Filter by cluster.cluster_id", - []string{"cluster.cluster_id=test1-managed1-fake-uuid-1,test6-managed6-fake-uuid-6"}, - // titles + actual data - 4, - ), - Entry( - "Filter by cluster.name", - []string{"cluster.name=managed1,managed6"}, - 4, - ), - Entry( - "Filter by event.compliance", - []string{"event.compliance=Compliant"}, - 7, - ), - Entry( - "Filter by event.message", - []string{"event.message=configmaps%20%5Bcommon%5D%20not%20found%20in%20namespace%20default"}, - 4, - ), - Entry( - "Filter by event.message_includes", - []string{"event.message_includes=etcd"}, - 4, - ), - Entry( - "Filter by event.message_like", - []string{"event.message_like=configmaps%20%5B%25common%25%5D%25"}, - 9, - ), - Entry( - "Filter by event.timestamp", - []string{"event.timestamp=2023-01-01T01:01:01.111Z"}, - 2, - ), - Entry( - "Filter by event.timestamp_after", - []string{"event.timestamp_after=2023-04-01T01:01:01.111Z"}, - 6, - ), - Entry( - "Filter by event.timestamp_before", - []string{"event.timestamp_before=2023-04-01T01:01:01.111Z"}, - 7, - ), - Entry( - "Filter by event.timestamp_after and event.timestamp_before", - []string{ - "event.timestamp_after=2023-01-01T01:01:01.111Z", - "event.timestamp_before=2023-04-01T01:01:01.111Z", - }, - 6, - ), - Entry( - "Filter by parent_policy.categories", - []string{"parent_policy.categories=cat-1,cat-3"}, - 5, - ), - Entry( - "Filter by parent_policy.controls", - []string{"parent_policy.controls=ctrl-2"}, - 4, - ), - Entry( - "Filter by parent_policy.id", - []string{"parent_policy.id=2"}, - 4, - ), - Entry( - "Filter by parent_policy.name", - []string{"parent_policy.name=etcd-encryption1"}, - 2, - ), - Entry( - "Filter by parent_policy.namespace", - []string{"parent_policy.namespace=policies"}, - 10, - ), - Entry( - "Filter by parent_policy.standards", - []string{"parent_policy.standards=stand-2"}, - 4, - ), - Entry( - "Filter by policy.apiGroup", - []string{"policy.apiGroup=policy.open-cluster-management.io"}, - 12, - ), - Entry( - "Filter by policy.apiGroup no results", - []string{"policy.apiGroup=does-not-exist"}, - 1, - ), - Entry( - "Filter by policy.id", - []string{"policy.id=4"}, - 4, - ), - Entry( - "Filter by policy.kind", - []string{"policy.kind=ConfigurationPolicy"}, - 12, - ), - Entry( - "Filter by policy.kind no results", - []string{"policy.kind=something-else"}, - 1, - ), - Entry( - "Filter by policy.name", - []string{"policy.name=common-b"}, - 3, - ), - Entry( - "Filter by policy.namespace", - []string{"policy.namespace=default"}, - 2, - ), - Entry( - "Filter by policy.severity", - []string{"policy.severity=low"}, - 10, - ), - Entry( - "Filter by policy.severity is null", - []string{"policy.severity"}, - 3, - ), - ) - }) - }) - - Describe("Duplicate compliance event", func() { - payload1 := []byte(`{ - "cluster": { - "name": "managed2", - "cluster_id": "test2-managed2-fake-uuid-2" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "duplicate-test", - "spec": {"test": "two"} - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [etcd] not found in namespace default", - "timestamp": "2023-02-02T02:02:02.222Z" - } - }`) - - BeforeAll(func(ctx context.Context) { - By("POST the initial event") - Eventually(postEvent(ctx, payload1, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - - It("Should fail when posting the same compliance event", func(ctx context.Context) { - err := postEvent(ctx, payload1, clientToken) - Expect(err).To(MatchError(ContainSubstring("The compliance event already exists"))) - }) - }) - - Describe("Large values", func() { - It("Should allow a large spec and message", func(ctx context.Context) { - longString := "" - charset := []rune{ - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - } - - for i := 0; i < 100000; i++ { - c := charset[rand.Intn(len(charset))] - longString += string(c) - } - - payload := []byte(fmt.Sprintf(`{ - "cluster": { - "name": "managed2", - "cluster_id": "test2-managed2-fake-uuid-2" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "duplicate-test", - "spec": {"test": "%s"} - }, - "event": { - "compliance": "NonCompliant", - "message": "%s", - "timestamp": "2023-02-02T02:02:02.222Z" - } - }`, longString, longString)) - - By("POST the event") - Eventually(postEvent(ctx, payload, clientToken), "5s", "1s").ShouldNot(HaveOccurred()) - }) - }) - - Describe("Test authorization", func() { - Describe("Test method Get", func() { - It("Should return unauthorized when it is empty token", func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint+"/1", nil) - Expect(err).ToNot(HaveOccurred()) - - res, err := httpClient.Do(req) - Expect(res.StatusCode).Should(Equal(http.StatusUnauthorized)) - Expect(err).ShouldNot(HaveOccurred()) - - req, err = http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint, nil) - Expect(err).ToNot(HaveOccurred()) - - res, err = httpClient.Do(req) - Expect(res.StatusCode).Should(Equal(http.StatusUnauthorized)) - Expect(err).ShouldNot(HaveOccurred()) - - req, err = http.NewRequestWithContext(ctx, http.MethodGet, csvEndpoint, nil) - Expect(err).ToNot(HaveOccurred()) - - res, err = httpClient.Do(req) - Expect(res.StatusCode).Should(Equal(http.StatusUnauthorized)) - Expect(err).ShouldNot(HaveOccurred()) - }) - It("Should return empty data when SA does not have any GET verb to managedCluster", - func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Content-Type", "application/json") - // Set auth token - req.Header.Set("Authorization", "Bearer "+wrongSAToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ShouldNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ShouldNot(HaveOccurred()) - - rows, ok := respJSON["data"].([]interface{}) - Expect(ok).To(BeTrue()) - - By("Should return 0 rows") - Expect(rows).Should(BeEmpty()) - }) - - It("Should return empty data when only unknown cluster IDs are provided", - func(ctx context.Context) { - endpoint := eventsEndpoint + "?cluster.cluster_id=does-not-exist,does-also-not-exist" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - Expect(err).ShouldNot(HaveOccurred()) - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ShouldNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ShouldNot(HaveOccurred()) - - rows, ok := respJSON["data"].([]interface{}) - Expect(ok).To(BeTrue()) - - By("Should return 0 rows") - Expect(rows).Should(BeEmpty()) - }, - ) - - It("Should return a forbidden error when SA has only managed1 auth", - func(ctx context.Context) { - argument := "cluster.name=managed1,managed2,managed3" - - By("governance-policy-propagator SA Should be able to access all") - - respJSON, err := listEvents(ctx, clientToken, argument) - Expect(err).ShouldNot(HaveOccurred()) - - data, ok := respJSON["data"].([]any) - Expect(ok).Should(BeTrue()) - - By("Should include at least managed2 or managed3") - hasVariousClusters := false - for _, d := range data { - complianceEvent, ok := d.(map[string]interface{}) - Expect(ok).To(BeTrue()) - - name, ok := complianceEvent["cluster"].(map[string]interface{})["name"].(string) - Expect(ok).To(BeTrue()) - - if name == "managed2" || name == "managed3" { - hasVariousClusters = true - - break - } - } - Expect(hasVariousClusters).Should(BeTrue()) - - for _, endpoint := range []string{eventsEndpoint, csvEndpoint} { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"?"+argument, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Content-Type", "application/json") - // Set auth token - req.Header.Set("Authorization", "Bearer "+subsetSAToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ShouldNot(HaveOccurred()) - - respJSON = map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ShouldNot(HaveOccurred()) - - message, ok := respJSON["message"].(string) - Expect(ok).To(BeTrue()) - - Expect(message). - Should(Equal("the request is not allowed: the following cluster filters are not " + - "authorized: managed2, managed3")) - - Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) - Expect(err).ShouldNot(HaveOccurred()) - } - }) - It("Should return a forbidden error when only unauthorized ID are passed as id", - func(ctx context.Context) { - argument := "cluster.cluster_id=wrong-id,test1-managed1-fake-uuid-1,test2-managed2-fake-uuid-2" - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint+"?"+argument, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Content-Type", "application/json") - // Set auth token - req.Header.Set("Authorization", "Bearer "+subsetSAToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ShouldNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ShouldNot(HaveOccurred()) - - message, ok := respJSON["message"].(string) - Expect(ok).To(BeTrue()) - - By("The error message should include test2-managed2-fake-uuid-2 except managed1") - Expect(message). - Should(Equal( - "the request is not allowed: the following cluster filters are not authorized: " + - "test2-managed2-fake-uuid-2")) - - Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) - Expect(err).ShouldNot(HaveOccurred()) - }) - It("Should return managed1 with subset SA when the query is empty", - func(ctx context.Context) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, eventsEndpoint, nil) - Expect(err).ShouldNot(HaveOccurred()) - - req.Header.Set("Content-Type", "application/json") - // Set auth token - req.Header.Set("Authorization", "Bearer "+subsetSAToken) - - resp, err := httpClient.Do(req) - Expect(err).ShouldNot(HaveOccurred()) - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - Expect(err).ShouldNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - Expect(err).ShouldNot(HaveOccurred()) - - rows, ok := respJSON["data"].([]interface{}) - Expect(ok).To(BeTrue()) - - By("Should return only managed1") - Expect(rows).Should(HaveLen(1)) - - id, ok := rows[0].(map[string]interface{})["id"].(float64) - Expect(ok).To(BeTrue()) - - Expect(int(id)).Should(Equal(1)) - }) - }) - }) -}) - -var _ = Describe("Test query generation", Label("compliance-events-api"), func() { - It("Tests the select query for a cluster", func() { - cluster := complianceeventsapi.Cluster{ - ClusterID: "my-cluster-id", - Name: "my-cluster", - } - sql, vals := cluster.SelectQuery("id", "spec") - Expect(sql).To(Equal("SELECT id, spec FROM clusters WHERE cluster_id=$1 AND name=$2")) - Expect(vals).To(HaveLen(2)) - }) - - It("Tests the select query for a minimum parent policy", func() { - parent := complianceeventsapi.ParentPolicy{ - Name: "parent-a", - Namespace: "policies", - } - sql, vals := parent.SelectQuery("id", "spec") - Expect(sql).To(Equal( - "SELECT id, spec FROM parent_policies WHERE name=$1 AND namespace=$2 AND categories IS NULL AND " + - "controls IS NULL AND standards IS NULL", - )) - Expect(vals).To(HaveLen(2)) - }) - - It("Tests the select query for a parent policy with all options", func() { - parent := complianceeventsapi.ParentPolicy{ - Name: "parent-a", - Namespace: "policies", - Categories: pq.StringArray{"cat-1"}, - Controls: pq.StringArray{"control-1", "control-2"}, - Standards: pq.StringArray{"standard-1"}, - } - sql, vals := parent.SelectQuery("id") - Expect(sql).To(Equal( - "SELECT id FROM parent_policies WHERE name=$1 AND namespace=$2 AND categories=$3 AND controls=$4 " + - "AND standards=$5", - )) - Expect(vals).To(HaveLen(5)) - }) - - It("Tests the select query for a minimum policy", func() { - policy := complianceeventsapi.Policy{ - Name: "parent-a", - Kind: "ConfigurationPolicy", - APIGroup: "policy.open-cluster-management.io", - Spec: complianceeventsapi.JSONMap{"spec": "this-out"}, - } - sql, vals := policy.SelectQuery("id") - Expect(sql).To(Equal( - "SELECT policies.id FROM policies LEFT JOIN specs ON policies.spec_id=specs.id WHERE api_group=$1 " + - "AND kind=$2 AND name=$3 AND spec=$4 AND namespace is NULL AND severity is NULL", - )) - Expect(vals).To(HaveLen(4)) - }) - - It("Tests the select query for a policy with all options", func() { - ns := "policies" - severity := "critical" - - policy := complianceeventsapi.Policy{ - Name: "parent-a", - Namespace: &ns, - Kind: "ConfigurationPolicy", - APIGroup: "policy.open-cluster-management.io", - Spec: complianceeventsapi.JSONMap{"spec": "this-out"}, - Severity: &severity, - } - sql, vals := policy.SelectQuery("id") - Expect(sql).To(Equal( - "SELECT policies.id FROM policies LEFT JOIN specs ON policies.spec_id=specs.id WHERE api_group=$1 " + - "AND kind=$2 AND name=$3 AND spec=$4 AND namespace=$5 AND severity=$6", - )) - Expect(vals).To(HaveLen(6)) - }) -}) - -func postEvent(ctx context.Context, payload []byte, token string) error { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, bytes.NewBuffer(payload)) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - errs := make([]error, 0) - - resp, err := httpClient.Do(req) - if err != nil { - errs = append(errs, err) - } - - if resp != nil { - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - errs = append(errs, err) - } - - if resp.StatusCode != http.StatusCreated { - errs = append(errs, fmt.Errorf("Got non-201 status code %v; response: %q", resp.StatusCode, string(body))) - } - } - - return errors.Join(errs...) -} - -func listEvents(ctx context.Context, token string, queryArgs ...string) (map[string]any, error) { - url := eventsEndpoint - - if len(queryArgs) > 0 { - url += "?" + strings.Join(queryArgs, "&") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - - // Set auth token - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return respJSON, fmt.Errorf("Got non-200 status code %v; response: %q", resp.StatusCode, string(body)) - } - - return respJSON, nil -} - -func getToken(ctx context.Context, ns, saName string) string { - secret := &v1.Secret{} - var err error - - Eventually(func(g Gomega) error { - secret, err = clientHub.CoreV1().Secrets(ns). - Get(ctx, saName, metav1.GetOptions{}) - - _, ok := secret.Data["token"] - g.Expect(ok).Should(BeTrue()) - - return err - }).ShouldNot(HaveOccurred()) - - _, ok := secret.Data["token"] - Expect(ok).Should(BeTrue()) - - return string(secret.Data["token"]) -} diff --git a/test/e2e/case1_propagation_test.go b/test/e2e/case1_propagation_test.go index 4bc4c54c..68722fc6 100644 --- a/test/e2e/case1_propagation_test.go +++ b/test/e2e/case1_propagation_test.go @@ -498,9 +498,6 @@ var _ = Describe("Test policy propagation", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(rootPlc.Object["spec"])) }) @@ -573,9 +570,6 @@ var _ = Describe("Test policy propagation", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) diff --git a/test/e2e/case20_compliance_api_controller_test.go b/test/e2e/case20_compliance_api_controller_test.go deleted file mode 100644 index d5de8375..00000000 --- a/test/e2e/case20_compliance_api_controller_test.go +++ /dev/null @@ -1,518 +0,0 @@ -// Copyright Contributors to the Open Cluster Management project - -package e2e - -import ( - "bytes" - "context" - "database/sql" - "encoding/json" - "fmt" - "io" - "math/rand" - "net/http" - "time" - - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "open-cluster-management.io/governance-policy-propagator/controllers/complianceeventsapi" - "open-cluster-management.io/governance-policy-propagator/controllers/propagator" - "open-cluster-management.io/governance-policy-propagator/test/utils" -) - -var _ = Describe("Test governance-policy-database secret changes and DB annotations", Serial, Ordered, func() { - const ( - case20PolicyName string = "case20-policy" - case20PolicyYAML string = "../resources/case20_compliance_api_controller/policy.yaml" - ) - - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) - nsName := fmt.Sprintf("case20-%d", seededRand.Int31()) - - createCase20Policy := func(ctx context.Context) { - GinkgoHelper() - - By("Creating " + case20PolicyName) - utils.Kubectl("apply", "-f", case20PolicyYAML, "-n", nsName, "--kubeconfig="+kubeconfigHub) - plc := utils.GetWithTimeout( - clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, - ) - Expect(plc).NotTo(BeNil()) - - By("Patching the placement with decision of cluster local-cluster") - pld := utils.GetWithTimeout( - clientHubDynamic, - gvrPlacementDecision, - case20PolicyName, - nsName, - true, - defaultTimeoutSeconds, - ) - pld.Object["status"] = utils.GeneratePldStatus(pld.GetName(), pld.GetNamespace(), "local-cluster") - _, err := clientHubDynamic.Resource(gvrPlacementDecision).Namespace(nsName).UpdateStatus( - ctx, pld, metav1.UpdateOptions{}, - ) - Expect(err).ToNot(HaveOccurred()) - - By("Waiting for the replicated policy") - replicatedPolicy := utils.GetWithTimeout( - clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, - ) - Expect(replicatedPolicy).NotTo(BeNil()) - } - - BeforeAll(func(ctx context.Context) { - Expect(clientToken).ToNot(BeEmpty(), "Ensure you use the service account kubeconfig (kubeconfig_hub)") - - By("Creating a random namespace to avoid a cache hit") - ns := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: nsName, - }, - } - _, err := clientHub.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) - Expect(err).ToNot(HaveOccurred()) - }) - - AfterEach(func(ctx context.Context) { - restoreDBConnection(ctx) - }) - - AfterAll(func(ctx context.Context) { - err := clientHub.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{}) - Expect(err).ToNot(HaveOccurred()) - }) - - It("Adds missing database IDs once database connection is restored", func(ctx context.Context) { - By("Updating the connection to be invalid") - bringDownDBConnection(ctx) - - createCase20Policy(ctx) - - By("Getting the replicated policy") - replicatedPolicy := utils.GetWithTimeout( - clientHubDynamic, gvrPolicy, case20PolicyName, nsName, true, defaultTimeoutSeconds, - ) - Expect(replicatedPolicy).NotTo(BeNil()) - - annotations := replicatedPolicy.GetAnnotations() - Expect(annotations[propagator.ParentPolicyIDAnnotation]).To(BeEmpty()) - Expect(annotations[propagator.PolicyIDAnnotation]).To(BeEmpty()) - - By("Restoring the database connection") - Eventually(func(g Gomega) { - namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") - secret, err := namespacedSecret.Get( - ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, - ) - g.Expect(err).ToNot(HaveOccurred()) - - delete(secret.Data, "port") - - _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - }, defaultTimeoutSeconds, 1).Should(Succeed()) - - By("Waiting for the replicated policy to have the database ID annotations") - Eventually(func(g Gomega) { - replicatedPolicy = utils.GetWithTimeout( - clientHubDynamic, gvrPolicy, nsName+"."+case20PolicyName, "local-cluster", true, defaultTimeoutSeconds, - ) - g.Expect(replicatedPolicy).NotTo(BeNil()) - - annotations = replicatedPolicy.GetAnnotations() - g.Expect(annotations[propagator.ParentPolicyIDAnnotation]).ToNot(BeEmpty()) - - templates, _, _ := unstructured.NestedSlice(replicatedPolicy.Object, "spec", "policy-templates") - g.Expect(templates).To(HaveLen(1)) - - policyID, _, _ := unstructured.NestedString( - templates[0].(map[string]interface{}), - "objectDefinition", - "metadata", - "annotations", - propagator.PolicyIDAnnotation, - ) - g.Expect(policyID).ToNot(BeEmpty()) - }, defaultTimeoutSeconds, 1).Should(Succeed()) - }) -}) - -func bringDownDBConnection(ctx context.Context) { - GinkgoHelper() - - By("Setting the port to 12345") - Eventually(func(g Gomega) { - namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") - secret, err := namespacedSecret.Get( - ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, - ) - g.Expect(err).ToNot(HaveOccurred()) - - secret.StringData = map[string]string{"port": "12345"} - - _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - }, defaultTimeoutSeconds, 1).Should(Succeed()) - - By("Waiting for the database connection to be down") - Eventually(func(g Gomega) { - req, err := http.NewRequestWithContext( - ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/api/v1/compliance-events/1", complianceAPIPort), nil, - ) - g.Expect(err).ToNot(HaveOccurred()) - - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - if err != nil { - return - } - - defer resp.Body.Close() - - g.Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError)) - - body, err := io.ReadAll(resp.Body) - g.Expect(err).ToNot(HaveOccurred()) - - respJSON := map[string]any{} - - err = json.Unmarshal(body, &respJSON) - g.Expect(err).ToNot(HaveOccurred()) - - g.Expect(respJSON["message"]).To(Equal("The database is unavailable")) - }, defaultTimeoutSeconds, 1).Should(Succeed()) -} - -func restoreDBConnection(ctx context.Context) { - GinkgoHelper() - - By("Restoring the database connection") - Eventually(func(g Gomega) { - namespacedSecret := clientHub.CoreV1().Secrets("open-cluster-management") - secret, err := namespacedSecret.Get( - ctx, complianceeventsapi.DBSecretName, metav1.GetOptions{}, - ) - g.Expect(err).ToNot(HaveOccurred()) - - if secret.Data["port"] == nil { - return - } - - delete(secret.Data, "port") - - _, err = namespacedSecret.Update(ctx, secret, metav1.UpdateOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - }, defaultTimeoutSeconds, 1).Should(Succeed()) - - By("Waiting for the database connection to be up") - Eventually(func(g Gomega) { - req, err := http.NewRequestWithContext( - ctx, - http.MethodGet, - fmt.Sprintf("https://localhost:%d/api/v1/compliance-events?per_page=1", complianceAPIPort), - nil, - ) - g.Expect(err).ToNot(HaveOccurred()) - - req.Header.Set("Authorization", "Bearer "+clientToken) - - resp, err := httpClient.Do(req) - if err != nil { - return - } - - defer resp.Body.Close() - - g.Expect(resp.StatusCode).To(Equal(http.StatusOK)) - }, defaultTimeoutSeconds, 1).Should(Succeed()) -} - -var _ = Describe("Test compliance events API authentication and authorization", Serial, Ordered, func() { - eventsEndpoint := fmt.Sprintf("https://localhost:%d/api/v1/compliance-events", complianceAPIPort) - const saName string = "compliance-api-user" - var token string - - getSamplePostRequest := func(clusterName string) *bytes.Buffer { - payload := []byte(fmt.Sprintf(`{ - "cluster": { - "name": "%s", - "cluster_id": "%s" - }, - "parent_policy": { - "name": "parent-policy", - "namespace": "%s" - }, - "policy": { - "apiGroup": "policy.open-cluster-management.io", - "kind": "ConfigurationPolicy", - "name": "etcd-encryption", - "spec": {"uid": "%s"} - }, - "event": { - "compliance": "NonCompliant", - "message": "configmaps [etcd] not found in namespace default", - "timestamp": "2023-02-02T02:02:02.222Z" - } - }`, clusterName, uuid.New().String(), uuid.New().String(), uuid.New().String())) - - return bytes.NewBuffer(payload) - } - - BeforeAll(func(ctx context.Context) { - By("Creating the service account " + saName + " in the namespace" + testNamespace) - _, err := clientHub.CoreV1().ServiceAccounts(testNamespace).Create( - ctx, - &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: saName, - Namespace: testNamespace, - }, - }, - metav1.CreateOptions{}, - ) - Expect(err).ToNot(HaveOccurred()) - - _, err = clientHub.CoreV1().Secrets(testNamespace).Create( - ctx, - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: saName, - Namespace: testNamespace, - Annotations: map[string]string{ - corev1.ServiceAccountNameKey: saName, - }, - }, - Type: corev1.SecretTypeServiceAccountToken, - }, - metav1.CreateOptions{}, - ) - Expect(err).ToNot(HaveOccurred()) - - By("Granting the service account " + saName + " permission to the cluster namespace " + testNamespace) - _, err = clientHub.RbacV1().Roles(testNamespace).Create( - ctx, - &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: saName, - Namespace: testNamespace, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{"policy.open-cluster-management.io"}, - Resources: []string{"policies/status"}, - Verbs: []string{"patch"}, - }, - }, - }, - metav1.CreateOptions{}, - ) - Expect(err).ToNot(HaveOccurred()) - - _, err = clientHub.RbacV1().RoleBindings(testNamespace).Create( - ctx, - &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: saName, - Namespace: testNamespace, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: saName, - Namespace: testNamespace, - }, - }, - RoleRef: rbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "Role", - Name: saName, - }, - }, - metav1.CreateOptions{}, - ) - Expect(err).ToNot(HaveOccurred()) - - Eventually(func(g Gomega) { - secret, err := clientHub.CoreV1().Secrets(testNamespace).Get(ctx, saName, metav1.GetOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - - g.Expect(secret.Data["token"]).ToNot(BeNil()) - - token = string(secret.Data["token"]) - }, defaultTimeoutSeconds, 1).Should(Succeed()) - }) - - AfterAll(func(ctx context.Context) { - By("Deleting the service account") - err := clientHub.CoreV1().ServiceAccounts(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) - if !k8serrors.IsNotFound(err) { - Expect(err).ToNot(HaveOccurred()) - } - - err = clientHub.CoreV1().Secrets(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) - if !k8serrors.IsNotFound(err) { - Expect(err).ToNot(HaveOccurred()) - } - - err = clientHub.RbacV1().Roles(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) - if !k8serrors.IsNotFound(err) { - Expect(err).ToNot(HaveOccurred()) - } - - err = clientHub.RbacV1().RoleBindings(testNamespace).Delete(ctx, saName, metav1.DeleteOptions{}) - if !k8serrors.IsNotFound(err) { - Expect(err).ToNot(HaveOccurred()) - } - - By("Deleting all database records") - connectionURL := "postgresql://grc:grc@localhost:5432/ocm-compliance-history?sslmode=disable" - db, err := sql.Open("postgres", connectionURL) - DeferCleanup(func() { - Expect(db.Close()).To(Succeed()) - }) - - Expect(err).ToNot(HaveOccurred()) - - _, err = db.ExecContext(ctx, "DELETE FROM compliance_events") - Expect(err).ToNot(HaveOccurred()) - _, err = db.ExecContext(ctx, "DELETE FROM clusters") - Expect(err).ToNot(HaveOccurred()) - _, err = db.ExecContext(ctx, "DELETE FROM parent_policies") - Expect(err).ToNot(HaveOccurred()) - _, err = db.ExecContext(ctx, "DELETE FROM policies") - Expect(err).ToNot(HaveOccurred()) - }) - - It("Rejects recording the compliance event without authentication", func(ctx context.Context) { - payload := getSamplePostRequest("cluster") - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized)) - }) - - It("Rejects recording the compliance event for the wrong namespace", func(ctx context.Context) { - payload := getSamplePostRequest("cluster") - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - Expect(resp.StatusCode).To(Equal(http.StatusForbidden)) - }) - - It("Allows recording the compliance event", func(ctx context.Context) { - payload := getSamplePostRequest(testNamespace) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - Expect(resp.StatusCode).To(Equal(http.StatusCreated)) - }) - - It("Clears its database ID cache when the database loses data", func(ctx context.Context) { - By("Creating a compliance event") - payloadStr := getSamplePostRequest(testNamespace).String() - payload := bytes.NewBufferString(payloadStr) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - Expect(resp.StatusCode).To(Equal(http.StatusCreated)) - - By("Deleting all compliance events and policy references") - connectionURL := "postgresql://grc:grc@localhost:5432/ocm-compliance-history?sslmode=disable" - db, err := sql.Open("postgres", connectionURL) - DeferCleanup(func() { - Expect(db.Close()).To(Succeed()) - }) - - Expect(err).ToNot(HaveOccurred()) - - _, err = db.ExecContext(ctx, "DELETE FROM compliance_events") - Expect(err).ToNot(HaveOccurred()) - _, err = db.ExecContext(ctx, "DELETE FROM parent_policies") - Expect(err).ToNot(HaveOccurred()) - _, err = db.ExecContext(ctx, "DELETE FROM policies") - Expect(err).ToNot(HaveOccurred()) - - By("Verifying an internal error is returned the first time an invalid ID is provided") - payload = bytes.NewBufferString(payloadStr) - req, err = http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - body, err := io.ReadAll(resp.Body) - Expect(err).ToNot(HaveOccurred()) - - Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError), fmt.Sprintf("Got response %s", string(body))) - - By("Verifying a success after the cache is cleared") - payload = bytes.NewBufferString(payloadStr) - req, err = http.NewRequestWithContext(ctx, http.MethodPost, eventsEndpoint, payload) - Expect(err).ToNot(HaveOccurred()) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = httpClient.Do(req) - Expect(err).ToNot(HaveOccurred()) - - if resp != nil { - defer resp.Body.Close() - } - - Expect(resp.StatusCode).To(Equal(http.StatusCreated)) - }) -}) diff --git a/test/e2e/case3_mutation_recovery_test.go b/test/e2e/case3_mutation_recovery_test.go index c7de27b3..de3c6556 100644 --- a/test/e2e/case3_mutation_recovery_test.go +++ b/test/e2e/case3_mutation_recovery_test.go @@ -142,9 +142,6 @@ var _ = Describe("Test unexpected policy mutation", func() { clientHubDynamic, gvrPolicy, testNamespace+"."+case3PolicyName, "managed2", true, defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(plc) - g.Expect(err).ToNot(HaveOccurred()) - return plc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(rootPlc.Object["spec"])) }) diff --git a/test/e2e/case6_placement_propagation_test.go b/test/e2e/case6_placement_propagation_test.go index df1f5eb9..a22c2a9e 100644 --- a/test/e2e/case6_placement_propagation_test.go +++ b/test/e2e/case6_placement_propagation_test.go @@ -438,9 +438,6 @@ var _ = Describe("Test policy propagation", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(rootPlc.Object["spec"])) }) @@ -522,9 +519,6 @@ var _ = Describe("Test policy propagation", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) diff --git a/test/e2e/case9_templates_test.go b/test/e2e/case9_templates_test.go index 1b54dea2..d4ee43b0 100644 --- a/test/e2e/case9_templates_test.go +++ b/test/e2e/case9_templates_test.go @@ -86,9 +86,6 @@ var _ = Describe("Test policy templates", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) @@ -109,9 +106,6 @@ var _ = Describe("Test policy templates", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) @@ -143,9 +137,6 @@ var _ = Describe("Test policy templates", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) @@ -236,9 +227,6 @@ var _ = Describe("Test policy templates", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) @@ -388,9 +376,6 @@ var _ = Describe("Test policy templates", func() { defaultTimeoutSeconds, ) - err := utils.RemovePolicyTemplateDBAnnotations(replicatedPlc) - g.Expect(err).ToNot(HaveOccurred()) - return replicatedPlc.Object["spec"] }, defaultTimeoutSeconds, 1).Should(utils.SemanticEqual(yamlPlc.Object["spec"])) }) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5b7560c1..3171062e 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -42,7 +42,6 @@ var ( clientHub kubernetes.Interface clientHubDynamic dynamic.Interface kubeconfigHub string - complianceAPIPort uint gvrPolicy schema.GroupVersionResource gvrPolicyAutomation schema.GroupVersionResource gvrPolicySet schema.GroupVersionResource @@ -69,13 +68,6 @@ func init() { klog.InitFlags(nil) flag.StringVar(&kubeconfigHub, "kubeconfig_hub", "../../kubeconfig_hub_e2e", "Location of the kubeconfig to use; defaults to current kubeconfig if set to an empty string") - flag.UintVar( - &complianceAPIPort, - "compliance-api-port", - 8384, - "The port of the Compliance API port; override when running the API locally and with a running Kind cluster "+ - "to avoid port conflicts", - ) } var _ = BeforeSuite(func() { diff --git a/test/resources/case18_compliance_api_test/subset_service_account.yaml b/test/resources/case18_compliance_api_test/subset_service_account.yaml deleted file mode 100644 index 552638cd..00000000 --- a/test/resources/case18_compliance_api_test/subset_service_account.yaml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subset-sa - namespace: default ---- -apiVersion: v1 -kind: Secret -type: kubernetes.io/service-account-token -metadata: - name: subset-sa - annotations: - kubernetes.io/service-account.name: subset-sa ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - creationTimestamp: null - name: subset-cluster-role -rules: -- apiGroups: - - '*' - resources: - - '*' - verbs: - - watch -- apiGroups: - - 'cluster.open-cluster-management.io' - resources: - - managedclusters - resourceNames: - - managed1 - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: subset-cluster-role-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: subset-cluster-role -subjects: -- kind: ServiceAccount - name: subset-sa - namespace: default \ No newline at end of file diff --git a/test/resources/case18_compliance_api_test/wrong_service_account.yaml b/test/resources/case18_compliance_api_test/wrong_service_account.yaml deleted file mode 100644 index 936d5cb3..00000000 --- a/test/resources/case18_compliance_api_test/wrong_service_account.yaml +++ /dev/null @@ -1,65 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: wrong-sa - namespace: default ---- -apiVersion: v1 -kind: Secret -type: kubernetes.io/service-account-token -metadata: - name: wrong-sa - annotations: - kubernetes.io/service-account.name: wrong-sa ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: wrong-cluster-role -rules: -- apiGroups: - - '*' - resources: - - '*' - verbs: - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: wrong-cluster-role-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: wrong-cluster-role -subjects: -- kind: ServiceAccount - name: wrong-sa - namespace: default ---- -# This ensures role bindings (not cluster role bindings) are ignored in the test -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: wrong-cluster-role-bound-with-role-binding -rules: -- apiGroups: - - cluster.open-cluster-management.io - resources: - - managedclusters - verbs: - - get ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: wrong-cluster-role-binding - namespace: default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: wrong-cluster-role-bound-with-role-binding -subjects: -- kind: ServiceAccount - name: wrong-sa - namespace: default \ No newline at end of file diff --git a/test/resources/case20_compliance_api_controller/policy.yaml b/test/resources/case20_compliance_api_controller/policy.yaml deleted file mode 100644 index 05ffbfc2..00000000 --- a/test/resources/case20_compliance_api_controller/policy.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: policy.open-cluster-management.io/v1 -kind: Policy -metadata: - name: case20-policy - annotations: - policy.open-cluster-management.io/categories: SI System and Information Integrity, SC System and Communications Protection - policy.open-cluster-management.io/standards: NIST SP 800-53 - policy.open-cluster-management.io/controls: SI-4 Information System Monitoring, SC-28 Protection Of Information At Rest -spec: - disabled: false - policy-templates: - - objectDefinition: - apiVersion: policy.open-cluster-management.io/v1 - kind: ConfigurationPolicy - metadata: - name: case20-policy - spec: - severity: low ---- -apiVersion: policy.open-cluster-management.io/v1 -kind: PlacementBinding -metadata: - name: case20-policy -placementRef: - apiGroup: cluster.open-cluster-management.io - kind: Placement - name: case20-policy -subjects: -- apiGroup: policy.open-cluster-management.io - kind: Policy - name: case20-policy ---- -apiVersion: cluster.open-cluster-management.io/v1beta1 -kind: Placement -metadata: - name: case20-policy -spec: - predicates: - - requiredClusterSelector: - labelSelector: - matchExpressions: [] ---- -apiVersion: cluster.open-cluster-management.io/v1beta1 -kind: PlacementDecision -metadata: - name: case20-policy - labels: - cluster.open-cluster-management.io/placement: case20-policy diff --git a/test/utils/utils.go b/test/utils/utils.go index f2104567..b3c00ddb 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -25,8 +25,6 @@ import ( "k8s.io/client-go/kubernetes" clusterv1beta1 "open-cluster-management.io/api/cluster/v1beta1" appsv1 "open-cluster-management.io/multicloud-operators-subscription/pkg/apis/apps/placementrule/v1" - - "open-cluster-management.io/governance-policy-propagator/controllers/propagator" ) // GeneratePlrStatus generate plr status with given clusters @@ -57,56 +55,6 @@ func GeneratePldStatus( return &clusterv1beta1.PlacementDecisionStatus{Decisions: plrDecision} } -func RemovePolicyTemplateDBAnnotations(plc *unstructured.Unstructured) error { - // Remove the database annotation since this can be an inconsistent value - templates, _, _ := unstructured.NestedSlice(plc.Object, "spec", "policy-templates") - - updated := false - - for i, template := range templates { - template := template.(map[string]interface{}) - - annotations, ok, _ := unstructured.NestedMap( - template, "objectDefinition", "metadata", "annotations", - ) - if !ok { - continue - } - - annotationVal, ok := annotations[propagator.PolicyIDAnnotation].(string) - if !ok { - continue - } - - if annotationVal != "" { - delete(annotations, propagator.PolicyIDAnnotation) - - if len(annotations) == 0 { - unstructured.RemoveNestedField(template, "objectDefinition", "metadata", "annotations") - } else { - err := unstructured.SetNestedField( - template, annotations, "objectDefinition", "metadata", "annotations", - ) - if err != nil { - return err - } - } - - templates[i] = template - updated = true - } - } - - if updated { - err := unstructured.SetNestedField(plc.Object, templates, "spec", "policy-templates") - if err != nil { - return err - } - } - - return nil -} - // Pause sleep for given seconds func Pause(s uint) { if s < 1 {