Skip to content

feat(co-importer): CO to ACS scan config importer#19636

Draft
guzalv wants to merge 23 commits intomasterfrom
feat/co-importer
Draft

feat(co-importer): CO to ACS scan config importer#19636
guzalv wants to merge 23 commits intomasterfrom
feat/co-importer

Conversation

@guzalv
Copy link
Contributor

@guzalv guzalv commented Mar 26, 2026

Description

Standalone CLI tool that reads Compliance Operator ScanSettingBinding resources from Kubernetes clusters and creates equivalent scan configurations in ACS via the v2 API.

Problem: Migrating from Compliance Operator scheduled scans to ACS-managed compliance scans requires manually recreating each scan configuration. For clusters with many ScanSettingBindings across multiple clusters, this is tedious and error-prone.

Solution: A standalone Go binary (compliance-operator-importer) under scripts/compliance-operator-importer/ that:

  • Reads ScanSettingBindings and their referenced ScanSettings from one or more Kubernetes clusters
  • Maps CO cron schedules and profile references to ACS scan configuration payloads
  • Creates equivalent scan configurations in ACS via the v2 API
  • Adopts SSBs by patching their settingsRef to ACS-managed ScanSettings
  • Merges same-name SSBs across clusters into a single ACS scan config

Usage

# Binary
ROX_API_TOKEN=... ./compliance-operator-importer \
  --endpoint central.example.com \
  --dry-run

# Container (via wrapper script)
ROX_API_TOKEN=... ./run.sh --endpoint central.example.com --dry-run

# Multi-cluster
KUBECONFIG=a.yaml:b.yaml ROX_API_TOKEN=... ./run.sh \
  --endpoint central.example.com

Key features

  • Dry-run mode for previewing actions
  • Idempotent: skips existing scan configs by default
  • --overwrite-existing to update existing configs
  • Multi-cluster merging with conflict detection
  • Auto-discovery of ACS cluster IDs
  • SSB adoption (patches settingsRef after import)
  • Pre-existing ScanSetting conflict detection
  • Container image with wrapper script for easy use
  • Retry with exponential backoff for transient errors
  • Detailed error messages including ACS API response bodies

Infrastructure changes

  • go.work + go.work.sum at repo root (separate Go module)
  • .golangci.yml exclusion for the importer path
  • tools/roxvet skip for non-rox packages

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • added unit tests
  • added e2e tests
  • added regression tests
  • added compatibility tests
  • modified existing tests

How I validated my change

  • Unit tests: go test ./... (all pass)
  • E2e tests against real OCP cluster with Compliance Operator installed
  • Tested single-cluster and multi-cluster modes
  • Tested dry-run and live modes
  • Tested idempotency (re-run skips correctly)
  • Tested adoption flow including pre-existing ScanSetting conflict detection
  • Tested container image via run.sh wrapper with single and multiple kubeconfigs
  • Verified error messages include ACS API response body details
  • Verified mapping warnings are printed for unsupported cron expressions

guzalv and others added 23 commits March 26, 2026 19:16
Standalone Go tool (scripts/compliance-operator-importer) that reads
Compliance Operator ScanSettingBinding/ScanSetting resources from a
Kubernetes cluster and creates equivalent ACS v2 compliance scan
configurations. Phase 1: create-only with idempotency (skip existing),
dry-run mode, exponential backoff retry, JSON report output, and
structured exit codes (0/1/2).

Infrastructure changes to support the standalone sub-module:
- go.work workspace file so go vet/staticcheck/roxvet can typecheck
  the importer module alongside the main rox module
- .golangci.yml: exclude importer path from linter path patterns
- tools/roxvet/validateimports: skip packages outside the rox module
  prefix instead of reporting them as errors (these appear via go.work
  when analyzing workspace packages from other modules)

Prompt: Build a standalone CO->ACS scheduled scan importer as a
complete Go sub-module under scripts/compliance-operator-importer/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ScanSettingBinding: profiles and settingsRef are top-level fields,
not nested under spec (spec is always empty in the actual CR).

ScanSetting: schedule is a top-level field, not nested under
complianceSuiteSettings.schedule as the spec document assumed.

Found during live smoke-test against the cluster.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ScanSetting.schedule is a top-level field, not nested under
complianceSuiteSettings.schedule as the spec originally assumed.
ScanSettingBinding.profiles is also top-level, not under spec.

Discovered during live smoke-test against the cluster.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns (Slice H)

Rename flags/env vars to match roxctl patterns: --endpoint/ROX_ENDPOINT,
ROX_API_TOKEN, ROX_ADMIN_PASSWORD, ROX_ADMIN_USER. Remove explicit
--acs-auth-mode in favor of auto-inference from which env var is set.
Unify --acs-cluster-id and --source-kubecontext into a single --cluster
flag that accepts UUID, name, or ctx= overrides. Add --overwrite-existing
for update-in-place (IMP-IDEM-008/009). Add --username with default "admin".

Specs updated to reflect the new contract. Extensive unit tests (~60 cases)
freeze the new behavior including edge cases.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hack/check-spec-coverage.sh extracts all IMP-* requirement IDs from
specs/ and verifies each has at least one matching test in *_test.go.
Handles range notation (IMP-CLI-001..016) and normalizes underscored
Go test names to hyphenated form for matching.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add e2e/e2e_test.go (build tag: e2e) and hack/run-e2e.sh wrapper.
Tests run against a real ACS + Compliance Operator cluster using
ROX_ENDPOINT and ROX_API_TOKEN/ROX_ADMIN_PASSWORD env vars.

Covers IMP-ACC-001 (CO resources listable), ACC-002 (auth preflight),
ACC-003 (dry-run no writes), ACC-004 (apply creates configs), ACC-005
(idempotent second run), ACC-007 (invalid token fails fast), ACC-012
(problems have fix hints), ACC-014 (overwrite updates), ACC-017
(auto-discover cluster ID).

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lice I)

Drop --kubeconfig, --kubecontext, and --cluster flags. The importer now
uses all contexts from the merged kubeconfig by default, with --context
(repeatable) as an opt-in filter. ACS cluster ID is always auto-discovered
via admission-control ConfigMap, OpenShift ClusterVersion, or Helm secret.

Specs cleaned: removed tombstone entries for deleted requirement IDs,
removed Old/New change tables, removed all temporal language referencing
past behavior that was never released.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… (IMP-CLI-016a)

When preflight fails due to a TLS certificate verification error (e.g.
self-signed cert), the error message now suggests --ca-cert-file or
--insecure-skip-verify instead of the generic "check network connectivity"
hint, which was misleading.

Adds spec requirement IMP-CLI-016a and a test that verifies the hint
content for untrusted certificates.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o backlog

Update traceability matrix with IMP-IMG-001..006 from the container
image spec. Add Slice J definition to the implementation backlog.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rence

Replace the spec-process-only README with a practical user guide covering
quick start, authentication, multi-cluster usage, flags reference,
mapping rules, exit codes, JSON report shape, and demo instructions.

Also ignore the built binary in .gitignore.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When all three discovery methods fail (admission-control ConfigMap,
OpenShift ClusterVersion, helm-effective-cluster-name Secret), the error
now lists each method's failure reason instead of a generic "all methods
failed" message. This makes it immediately clear whether the issue is
auth (e.g. expired kubeconfig credentials), missing resources, or
something else.

Before:
  all discovery methods failed to resolve ACS cluster ID

After:
  all discovery methods failed to resolve ACS cluster ID:
  - admission-control ConfigMap: ... Unauthorized
  - OpenShift ClusterVersion: ... Unauthorized
  - helm-effective-cluster-name Secret: ... Unauthorized

Adds spec scenario IMP-MAP-016a for the detailed error contract.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntial collisions

When KUBECONFIG lists multiple files (e.g. config:config-secured-cluster),
kubectl merges them into a single view. If both files define a user named
"admin" with different certificates, the merge silently picks one and the
other cluster gets wrong credentials — causing confusing auth failures.

The importer now loads each kubeconfig file in isolation via ExplicitPath,
so user/cluster/context entries in one file never interfere with another.
Duplicate context names across files are allowed and both are processed
with their own credentials.

Changes:
- cluster_source.go: split KUBECONFIG into individual files, load each
  independently via clientcmd.ExplicitPath, enumerate contexts per file
- cofetch/client.go: add NewClientFromRestConfig to accept a pre-built
  rest.Config (avoids re-loading merged kubeconfig)
- spec IMP-CLI-003: updated to specify per-file isolation semantics
- cluster_source_test.go: new tests for per-file loading, credential
  isolation, duplicate context handling, and --context filtering

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…20a)

Merge conflict problems (e.g. same-name SSBs with different profiles
across clusters) were collected into the report but never printed to
the console, leaving users with a cryptic "merged into 0" message and
no explanation. Add r.status.Warnf() for each merge problem so the
conflict reason is visible inline.

Also includes RunMultiCluster orchestrator (multi_cluster.go) and
updates specs:
- Fix merge conflict scenarios: category "mapping" → "conflict"
  (matches code and models.CategoryConflict)
- Add IMP-MAP-020a console output requirement
- Add adopt and wire-format spec scenarios
- Update traceability matrix with IMP-MAP-020a

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…side

The previous demo modified the ACS scan config schedule directly via
API, but after SSB adoption ACS pushes its schedule back down to the
cluster ScanSetting — so there was no real conflict visible.

Changed to simulate drift from the Kubernetes side: update the original
ScanSetting schedule (02:00 → 05:00) and patch the SSB's settingsRef
back to it.  Now the importer reads 05:00 from k8s while ACS has 02:00,
showing a genuine drift that --overwrite-existing resolves.

Also fixed cleanup to delete ACS-created ScanSettings (named after SSBs).

Tested end-to-end in non-interactive mode (DEMO_AUTO=1 DEMO_PAUSE=0).

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The drift scenario now patches the ACS-managed ScanSetting directly on
the cluster (kubectl patch scansetting <ssb-name>).  ACS does not detect
this change, so the UI still shows the original schedule while scans
actually run on the new one — a silent drift.

This is more realistic than the previous version (which patched the
SSB's settingsRef back to a different ScanSetting) and demonstrates
the exact gap the importer fills: reading the actual cluster state
and syncing ACS to match.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… (IMP-MAP-004a)

The ACS v2 Schedule proto defines daysOfWeek (repeated int32) not a
"weekly" wrapper.  The previous ACSWeekly struct serialized to
{"weekly":{"day":0}} which the gRPC gateway silently ignored — weekly
scans were treated as daily.

Replace ACSWeekly with ACSDaysOfWeek{Days []int32} matching the proto.
Add wire-format tests (IMP-MAP-004a..d) that serialize payloads to JSON
and assert field names match proto/api/v2/common.proto.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When CreateScanConfiguration or UpdateScanConfiguration returns a non-2xx
status, the error message now includes a snippet of the response body.
This makes it clear *why* the ACS API rejected the request (e.g.
"Unable to find all profiles for scan configuration").

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…PT-001..008)

After the importer creates a scan config in ACS, ACS pushes a
ScanSetting to the cluster with the same name.  The SSB still
references the old ScanSetting, so it's not managed by ACS yet.

The adoption step:
1. Polls for the ACS-created ScanSetting to appear on the cluster
2. Patches the SSB's settingsRef.name to the new ScanSetting
3. Handles timeouts (warning, not error) and partial multi-cluster
   success independently per cluster

New packages:
- internal/adopt: adoption logic with poll/timeout/patch
- internal/merge: SSB merging across clusters with conflict detection
- internal/status: stage-by-stage console progress output

Also adds PatchSSBSettingsRef to the COClient interface and wires
adoption into both single-cluster (Run) and multi-cluster
(RunMultiCluster) paths.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Dockerfile: multi-stage build with ubi9-micro base, CGO_ENABLED=0
  static binary, multi-arch support (amd64+arm64)
- Makefile: build, test, lint, image targets
- .dockerignore: exclude test fixtures and vendor from build context
- Spec 07: container image requirements and acceptance criteria

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hack/demo-seed.sh creates ScanSetting + ScanSettingBindings on the
current cluster for demo/testing purposes.  Tracks the last seed ID
in hack/.demo-seed-id to enable cleanup on re-run.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…output

Three issues fixed:

1. Non-transient HTTP errors (400, 401, etc.) only showed the status code
   in the Reason field but not the response body detail. Now Reason
   includes the full error message from the ACS API.

2. Multi-cluster fail path printed action.Reason instead of action.Err,
   losing the response body. Now checks action.Err first, matching the
   single-cluster code path.

3. Multi-cluster mapping/ScanSetting errors were collected into the
   problem collector but never printed to the console, causing SSBs to
   be silently dropped. Now emits Warnf for both ScanSetting fetch
   failures and mapping errors (e.g. unsupported cron step notation).

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrapper script that auto-mounts kubeconfig files and forwards ACS auth
env vars so running via container is a one-liner:

  ROX_API_TOKEN=... ./run.sh --endpoint central.example.com --dry-run

Handles KUBECONFIG with multiple colon-separated paths, forwards
ROX_ENDPOINT, ROX_API_TOKEN, ROX_ADMIN_PASSWORD, ROX_ADMIN_USER.
Supports IMAGE and CONTAINER_RT overrides.

Also reverts Makefile IMAGE default to localhost/ (local build target).

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… cluster

When the importer creates a scan config in ACS, ACS pushes a ScanSetting
to the cluster. The adoption step then patches the SSB's settingsRef to
point to it. But if a ScanSetting with that name already existed before
reconciliation, the adoption poll would find it immediately and patch the
SSB onto a resource ACS doesn't control.

Fix: snapshot which ScanSettings (named after SSBs) exist on each cluster
before reconciliation. During adoption, if the target name was already in
the snapshot, skip with a warning instead of patching.

Partially generated by AI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gitguardian
Copy link

gitguardian bot commented Mar 26, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
29242324 Triggered Username Password 84b000a scripts/compliance-operator-importer/internal/config/config_test.go View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@openshift-ci
Copy link

openshift-ci bot commented Mar 26, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@codecov
Copy link

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 63.04478% with 619 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.43%. Comparing base (2cad81c) to head (e0dea1e).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...ce-operator-importer/internal/run/multi_cluster.go 0.00% 185 Missing ⚠️
...ompliance-operator-importer/internal/acs/client.go 37.56% 109 Missing and 19 partials ⚠️
...ce-operator-importer/internal/discover/discover.go 38.61% 60 Missing and 2 partials ⚠️
...e-operator-importer/internal/run/cluster_source.go 45.09% 52 Missing and 4 partials ⚠️
...perator-importer/internal/reconcile/create_only.go 65.27% 48 Missing and 2 partials ⚠️
...mpliance-operator-importer/internal/merge/merge.go 69.02% 27 Missing and 8 partials ⚠️
...-operator-importer/internal/preflight/preflight.go 70.90% 28 Missing and 4 partials ⚠️
...s/compliance-operator-importer/internal/run/run.go 82.79% 28 Missing and 4 partials ⚠️
...liance-operator-importer/internal/config/config.go 94.71% 10 Missing and 2 partials ⚠️
...nce-operator-importer/internal/mapping/schedule.go 84.41% 8 Missing and 4 partials ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19636      +/-   ##
==========================================
+ Coverage   49.32%   49.43%   +0.11%     
==========================================
  Files        2737     2751      +14     
  Lines      206445   208120    +1675     
==========================================
+ Hits       101823   102880    +1057     
- Misses      97075    97644     +569     
- Partials     7547     7596      +49     
Flag Coverage Δ
go-unit-tests 49.43% <63.04%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant