Skip to content

Conversation

@aaronschweig
Copy link
Contributor

@aaronschweig aaronschweig commented Oct 23, 2025

It generates a complete and valid schema with all kubernetes core types included:

Rendered FGA schema

model
  schema 1.2

type apis_kcp_io_apibinding # module: core, file: /default.fga
  relations
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: member from parent
    define owner: owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: owner
    define update: owner
    define watch: member

type core_platform-mesh_io_account # module: core, file: /default.fga
  relations
    define create_apis_kcp_io_apibindings: owner
    define create_core_namespaces: member
    define create_core_platform-mesh_io_accountinfos: member
    define create_core_platform-mesh_io_accounts: member
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define list__apis_kcp_io_apibindings: member
    define list_core_namespaces: member
    define list_core_platform-mesh_io_accountinfos: member
    define list_core_platform-mesh_io_accounts: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member
    define watch_apis_kcp_io_apibindings: member
    define watch_core_namespaces: member
    define watch_core_platform-mesh_io_accountinfos: member
    define watch_core_platform-mesh_io_accounts: member

type core_platform-mesh_io_accountinfo # module: core, file: /default.fga
  relations
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: member from parent
    define owner: owner from parent
    define parent: [core_platform-mesh_io_account]
    define watch: member

type role # module: core, file: /default.fga
  relations
    define assignee: [user, user:*]

type user # module: core, file: /default.fga

type core_clusterrolebinding # module: internal_core_types_clusterrolebindings, file: internal_core_types_clusterrolebindings.fga
  relations
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: owner
    define update: owner
    define watch: member

type core_clusterrole # module: internal_core_types_clusterroles, file: internal_core_types_clusterroles.fga
  relations
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: owner
    define update: owner
    define watch: member

type core_configmap # module: internal_core_types_configmaps, file: internal_core_types_configmaps.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_event # module: internal_core_types_events, file: internal_core_types_events.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_localsubjectaccessreview # module: internal_core_types_localsubjectaccessreviews, file: internal_core_types_localsubjectaccessreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_namespace # module: internal_core_types_namespaces, file: internal_core_types_namespaces.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

type core_resourcequota # module: internal_core_types_resourcequotas, file: internal_core_types_resourcequotas.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_rolebinding # module: internal_core_types_rolebindings, file: internal_core_types_rolebindings.fga
  relations
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: owner
    define update: owner
    define watch: member

type core_role # module: internal_core_types_roles, file: internal_core_types_roles.fga
  relations
    define delete: owner
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: owner
    define update: owner
    define watch: member

type core_secret # module: internal_core_types_secrets, file: internal_core_types_secrets.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_selfsubjectaccessreview # module: internal_core_types_selfsubjectaccessreviews, file: internal_core_types_selfsubjectaccessreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

type core_selfsubjectreview # module: internal_core_types_selfsubjectreviews, file: internal_core_types_selfsubjectreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

type core_selfsubjectrulesreview # module: internal_core_types_selfsubjectrulesreviews, file: internal_core_types_selfsubjectrulesreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

type core_serviceaccount # module: internal_core_types_serviceaccounts, file: internal_core_types_serviceaccounts.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_namespace]
    define patch: member
    define update: member
    define watch: member

type core_subjectaccessreview # module: internal_core_types_subjectaccessreviews, file: internal_core_types_subjectaccessreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

type core_tokenreview # module: internal_core_types_tokenreviews, file: internal_core_types_tokenreviews.fga
  relations
    define delete: member
    define get: member
    define get_iam_roles: member
    define get_iam_users: member
    define manage_iam_roles: owner
    define member: [role#assignee] or owner or member from parent
    define owner: [role#assignee] or owner from parent
    define parent: [core_platform-mesh_io_account]
    define patch: member
    define update: member
    define watch: member

Summary by CodeRabbit

  • Chores

    • Added gnostic-models v0.7.0 to dependencies and removed token-based envtest auth setup.
  • New Features

    • Authorization model generation now incorporates dynamically discovered API types for org-scoped contexts.
  • Bug Fixes

    • Invite processing no longer errors when creator info is missing; it logs and safely no-ops.
  • Tests

    • Expanded and refactored test coverage and generated mocks to support discovery and controller interfaces.

@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Walkthrough

Injects a discovery-client factory into authorization-model generation to discover and render Kubernetes API resources; changes invite.Process to no-op when Account.Spec.Creator is nil; replaces mockery config and regenerates many mocks with new constructors; adds a direct go.mod dependency and several test updates.

Changes

Cohort / File(s) Summary
Dependency manifest
go.mod
Adds direct dependency github.com/google/gnostic-models v0.7.0 (removed from indirect require).
Authorization model subroutine
internal/subroutine/authorization_model.go
Adds NewDiscoveryClientFunc type and field on subroutine; extends constructor to accept discovery factory; performs discovery-driven rendering for non-"orgs" stores via new helpers discoverAndRender and processAPIResourceIntoModel; switches comparison to JSON-based equality and merges discovered modules into moduleFiles before transformation.
Controller wiring
internal/controller/store_controller.go
Injects a discovery-client factory (func(*rest.Config) discovery.DiscoveryInterface using discovery.NewDiscoveryClientForConfigOrDie) into NewAuthorizationModelSubroutine call.
Invite subroutine & tests
internal/subroutine/invite.go, internal/subroutine/invite_test.go
When account.Spec.Creator is nil, Process now logs and returns success (no-op) instead of error; corresponding test case removed and minor test formatting adjusted.
Mock config
.mockery.yaml
Moves from package-level outpkg to per-file filename entries; adds mock blocks (manager, discovery, etc.); switches to per-file naming and structname entries.
Generated mocks
internal/subroutine/mocks/*
Adds/updates many generated mocks with new NewMock... constructors requiring mock.TestingT+Cleanup, embed mock.Mock, rename receivers to _mock, introduce strongly-typed Run/Return/RunAndReturn helpers, nil-safety checks, and automatic t.Cleanup expectation assertions. Notable: mock_CTRLManager.go, mock_Client.go, mock_Cluster.go, mock_DiscoveryInterface.go, mock_Manager.go, mock_OpenFGAServiceClient.go, mock_Manager.go.
Tests & small unit additions
internal/subroutine/authorization_model_test.go, internal/subroutine/authorization_model_generation_test.go, internal/subroutine/realm_test.go, internal/subroutine/store_test.go, internal/subroutine/workspace_authorization_test.go
Update tests to pass discovery-factory parameter and use discovery mocks; add GetName/Finalizers unit tests; add additional error-path cases for store verification/update.
CI/taskfile
Taskfile.yaml
Removes PLATFORM_MESH_TOKEN usage and associated git-credential setup from the envtest task.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • nexus49
  • akafazov

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: automatic core type generation" is directly related to the main change in the changeset. The core functionality modification in authorization_model.go adds dynamic discovery of Kubernetes API resources and automatic generation of authorization schemas (FGA models) for those discovered types. The title accurately captures this primary feature—it uses specific terminology ("automatic," "core type," "generation") that describes what the PR accomplishes without being overly vague or misleading. A teammate reviewing the repository history would understand that this PR introduces automated schema generation for Kubernetes core types, which aligns with the stated PR objectives of "automatic generation of a complete, valid FGA schema that includes all Kubernetes core types."
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/automatic-core-type-generation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/subroutine/invite.go (1)

77-86: I need to verify the reconciler setup more comprehensively to confirm whether Account changes trigger reconciliation through any mechanism.

Now let me check the reconciler's setup method to confirm the watch configuration:

Let me now search the repository for the reconciler setup to confirm the watch configuration:

Based on my verification:

  1. Script execution confirmed: No explicit Watches or Owns on Account objects found in the codebase.
  2. Controllers require Watches to be configured to enqueue reconcile.Requests in response to events, and without an Account watch, RequeueAfter duration can be set in the Reconciler return object and will run again after that duration even if the object is unchanged.
  3. Go validation idiom confirmed: To check for strings that contain only whitespace, use strings.TrimSpace(str) == "", which is exactly what the suggested diff does after nil-checking the *string pointer.

The original review comment is well-founded. Without a Watch on Account, if Creator is set after the nil check, no automatic reconciliation occurs. Adding RequeueAfter: time.Minute ensures eventual consistency through periodic retries, and the TrimSpace check prevents silent failures with whitespace-only emails.


Add RequeueAfter and validate non-empty email to ensure eventual consistency and prevent stalled Invite creation.

When account.Spec.Creator is nil, the reconciler returns early. Without a watch on Account updates, if Creator is set later, reconciliation won't auto-trigger. Additionally, an empty or whitespace-only email bypasses validation. Apply this fix:

- if account.Spec.Creator == nil {
-   log.Info().Str("workspace", wsName).Msg("account creator is nil, skipping invite creation")
-   return ctrl.Result{}, nil
- }
+ if account.Spec.Creator == nil || strings.TrimSpace(*account.Spec.Creator) == "" {
+   log.Info().Str("workspace", wsName).Msg("account creator is nil/empty, skipping invite creation")
+   // Light backoff so we eventually pick up a late Creator without flooding the queue.
+   return ctrl.Result{RequeueAfter: time.Minute}, nil
+ }

Add imports:

import (
  // ...
  "strings"
  "time"
)
🧹 Nitpick comments (1)
internal/subroutine/authorization_model.go (1)

154-161: Preserve any existing host path prefix when scoping to a workspace.

Overwriting parsed.Path may drop an existing reverse-proxy prefix. Prefer joining with the existing path.

- parsed.Path, err = url.JoinPath("clusters", fmt.Sprintf("root:orgs:%s", store.Name))
+ parsed.Path, err = url.JoinPath(parsed.Path, "clusters", fmt.Sprintf("root:orgs:%s", store.Name))

If you need stricter control, derive from a clean URL base and set cfg.PathPrefix instead.

Please confirm your cluster config contains no required path prefix; otherwise requests may 404 behind proxies.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee754e and c6b7e0f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod (1 hunks)
  • internal/subroutine/authorization_model.go (4 hunks)
  • internal/subroutine/invite.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: pipe / dockerBuild / docker
  • GitHub Check: pipe / testSource / test
  • GitHub Check: pipe / lint / lint
🔇 Additional comments (3)
go.mod (1)

18-18: Module hygiene confirmed; dependency bump is clean.

The v0.7.3 bump is already properly synced in go.sum (lines 162-163). The release only includes a bug fix for status condition updates, with no breaking changes. No further action needed.

internal/subroutine/authorization_model.go (2)

30-32: Schema version "1.2" is correct and compatible.

OpenFGA supports schema version 1.2, and the openfga/language Go package v0.2.0-beta.2 examples demonstrate compatibility with schema "1.2". No changes needed.


268-293: The review comment misdiagnoses the actual issue and suggests an ineffective fix.

The real problem is not filename collision—it's that the template generates type definitions with group-prefixed names (e.g., authorization_k8s_io_pod) but hardcodes parent type references to only core_namespace and core_platform-mesh_io_account (lines 41, 58). These parent types don't exist in the codebase for non-core groups, causing generated modules from groups like authentication.k8s.io or authorization.k8s.io to reference undefined parent types.

Changing filenames does not affect the type names generated by the template—it only changes the module name wrapping. The suggestion leaves the actual bug (hardcoded parent references) unresolved. The template logic needs fixing, not the filename pattern.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
internal/subroutine/authorization_model.go (2)

258-264: Fallback when SingularName is empty and sanitize identifiers.

Some APIResources have empty SingularName; fall back to lowercased Kind and sanitize.

- err := tpl.Execute(&buffer, modelInput{
-   Name:     resource.Name,
-   Group:    strings.ReplaceAll(group, ".", "_"), // TODO: group name length capping
-   Singular: resource.SingularName,
+ singular := resource.SingularName
+ if singular == "" {
+   singular = strings.ToLower(resource.Kind)
+ }
+ err := tpl.Execute(&buffer, modelInput{
+   Name:     resource.Name,
+   Group:    strings.ReplaceAll(group, ".", "_"), // TODO: group name length capping
+   Singular: singular,
   Scope:    string(scope),
 })

38-71: Avoid module/file name collisions across groups (include group in names).

Current module header and file name use only resource.Name, which can collide across API groups. Include sanitized group.

- priviledgedTemplate = template.Must(template.New("model").Parse(`module internal_core_types_{{ .Name }}
+ priviledgedTemplate = template.Must(template.New("model").Parse(`module internal_core_types_{{ .Group }}_{{ .Name }}

And for generated files:

- files = append(files, language.ModuleFile{
-   Name:     fmt.Sprintf("internal_core_types_%s.fga", apiRes.Name),
+ group := apiRes.Group
+ if group == "" { group = "core" }
+ group = strings.ReplaceAll(group, ".", "_")
+ files = append(files, language.ModuleFile{
+   Name:     fmt.Sprintf("internal_core_types_%s_%s.fga", group, apiRes.Name),
    Contents: buf.String(),
 })

Apply the same module-name change to the core template referenced by modelTpl to keep headers and filenames aligned.

Also applies to: 290-294

🧹 Nitpick comments (5)
internal/subroutine/mocks/mock_OpenFGAServiceClient.go (1)

42-51: Standardize variadic grpc.CallOption handling (avoid mismatched expectations).

The methods package opts as a single []grpc.CallOption (Called(ctx, in, opts)), while the Expecter helpers expand them (append(..., opts...)...). If any production code passes options, expectations won’t match and Run handlers will panic on args[2].([]grpc.CallOption).

Align the Expecter to pass a single []grpc.CallOption (as the methods do). Example for BatchCheck:

func (_e *MockOpenFGAServiceClient_Expecter) BatchCheck(ctx interface{}, in interface{}, opts ...interface{}) *MockOpenFGAServiceClient_BatchCheck_Call {
-  return &MockOpenFGAServiceClient_BatchCheck_Call{Call: _e.mock.On("BatchCheck",
-    append([]interface{}{ctx, in}, opts...)...)}
+  // Pack variadic opts into a single slice to match method invocation.
+  var packed interface{}
+  if len(opts) > 0 {
+    packed = opts
+    return &MockOpenFGAServiceClient_BatchCheck_Call{Call: _e.mock.On("BatchCheck", ctx, in, packed)}
+  }
+  return &MockOpenFGAServiceClient_BatchCheck_Call{Call: _e.mock.On("BatchCheck", ctx, in)}
}

Please apply the same pattern across all methods with grpc.CallOption varargs.

Also applies to: 85-93, 125-134

internal/subroutine/invite_test.go (1)

65-84: Restore coverage for “Creator == nil” no-op path.

A test for Account.Spec.Creator == nil was removed. Please add a case asserting we do not create an Invite and return nil error. I can draft it if helpful.

internal/subroutine/mocks/mock_CTRLManager.go (1)

25-37: LGTM; add compile-time interface check to catch drift early.

Looks correct for controller-runtime’s Manager. Optionally add an interface assertion to fail fast if upstream signatures change.

+// Compile-time check: ensure CTRLManager satisfies controller-runtime manager.Manager.
+var _ manager.Manager = (*CTRLManager)(nil)
internal/subroutine/mocks/mock_Manager.go (1)

23-35: LGTM; add compile-time interface check to guard against upstream changes.

The surface matches multicluster-runtime Manager. Add assertion to fail fast if the API drifts.

+// Compile-time check: ensure MockManager satisfies multicluster manager.Manager.
+var _ manager.Manager = (*MockManager)(nil)
internal/subroutine/mocks/mock_DiscoveryInterface.go (1)

17-29: LGTM; add compile-time interface check for discovery.

Add a satisfaction assertion to catch future API drift.

+// Compile-time check: ensure MockDiscoveryInterface satisfies discovery.DiscoveryInterface.
+var _ discovery.DiscoveryInterface = (*MockDiscoveryInterface)(nil)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6b7e0f and 9029986.

📒 Files selected for processing (12)
  • .mockery.yaml (1 hunks)
  • go.mod (1 hunks)
  • internal/controller/store_controller.go (2 hunks)
  • internal/subroutine/authorization_model.go (4 hunks)
  • internal/subroutine/authorization_model_test.go (3 hunks)
  • internal/subroutine/invite_test.go (1 hunks)
  • internal/subroutine/mocks/mock_CTRLManager.go (1 hunks)
  • internal/subroutine/mocks/mock_Client.go (19 hunks)
  • internal/subroutine/mocks/mock_Cluster.go (20 hunks)
  • internal/subroutine/mocks/mock_DiscoveryInterface.go (1 hunks)
  • internal/subroutine/mocks/mock_Manager.go (24 hunks)
  • internal/subroutine/mocks/mock_OpenFGAServiceClient.go (21 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go.mod
🧰 Additional context used
🧬 Code graph analysis (2)
internal/controller/store_controller.go (1)
internal/subroutine/authorization_model.go (1)
  • NewAuthorizationModelSubroutine (83-90)
internal/subroutine/authorization_model_test.go (3)
internal/subroutine/authorization_model.go (1)
  • NewAuthorizationModelSubroutine (83-90)
internal/subroutine/mocks/mock_CTRLManager.go (1)
  • NewCTRLManager (27-37)
internal/subroutine/mocks/mock_DiscoveryInterface.go (1)
  • NewMockDiscoveryInterface (19-29)
🔇 Additional comments (5)
internal/subroutine/authorization_model_test.go (1)

57-70: Update tests for discovery factory returning (client, error).

If you adopt the error-returning factory, adjust constructor usages:

- subroutine := subroutine.NewAuthorizationModelSubroutine(nil, nil, nil, nil, nil)
+ subroutine := subroutine.NewAuthorizationModelSubroutine(nil, nil, nil, nil, nil)

(no change needed when passing nil)

And where a mock is provided:

- subroutine := subroutine.NewAuthorizationModelSubroutine(fga, manager, client, func(cfg *rest.Config) discovery.DiscoveryInterface { return discoveryMock }, logger.Logger)
+ subroutine := subroutine.NewAuthorizationModelSubroutine(
+   fga, manager, client,
+   func(cfg *rest.Config) (discovery.DiscoveryInterface, error) { return discoveryMock, nil },
+   logger.Logger,
+ )

Also applies to: 337-345

internal/subroutine/mocks/mock_Cluster.go (1)

20-32: LGTM on mock constructor and expecter pattern.

Constructor wiring and cleanup look correct.

.mockery.yaml (1)

1-9: YAML syntax is valid; manual verification of regeneration still required.

The .mockery.yaml configuration parses correctly with proper structure across all six package definitions. However, mockery CLI is unavailable in this environment, so you must manually verify that running mockery produces no unintended drift by executing:

mockery --config .mockery.yaml --all --keeptree --dry-run
git status --porcelain

This will confirm the current config regenerates the exact existing mock files without changes.

internal/subroutine/mocks/mock_Client.go (2)

44-66: Double-check obj type for Apply: runtime.ApplyConfiguration may not exist in apimachinery.

If you’re targeting controller-runtime v0.22.x, verify the exact obj type required by Client.Apply (if present). Mismatch will break builds.

If Apply is not on client.Client (or uses a different obj type), regenerate this mock against the correct interface version to avoid compile errors.


72-79: Fix variadic option mismatch: expectations expand opts, invocations pass a slice — leads to unmatched mocks.

Today:

  • Methods call _mock.Called(..., opts) (single slice).
  • Expecters call .On(..., append(..., opts...)...) (expanded).
  • Run handlers expect args[n].([]client.<Option>).

Result: when options are provided, the call signatures don’t match and tests will fail.

Minimal, safe fix: keep invocations passing a single typed slice; change Expecter helpers to also pass a single typed slice by converting ...interface{} to the correct []client.<Option> type.

Apply this pattern across all affected Expecters:

--- a/internal/subroutine/mocks/mock_Client.go
+++ b/internal/subroutine/mocks/mock_Client.go
@@
 func (_e *MockClient_Expecter) Apply(ctx interface{}, obj interface{}, opts ...interface{}) *MockClient_Apply_Call {
-  return &MockClient_Apply_Call{Call: _e.mock.On("Apply",
-    append([]interface{}{ctx, obj}, opts...)...)}
+  // Pack variadics into a single typed []client.ApplyOption to match invocation.
+  packed := make([]client.ApplyOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.ApplyOption))
+  }
+  return &MockClient_Apply_Call{Call: _e.mock.On("Apply", ctx, obj, packed)}
 }
@@
 func (_e *MockClient_Expecter) Create(ctx interface{}, obj interface{}, opts ...interface{}) *MockClient_Create_Call {
-  return &MockClient_Create_Call{Call: _e.mock.On("Create",
-    append([]interface{}{ctx, obj}, opts...)...)}
+  packed := make([]client.CreateOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.CreateOption))
+  }
+  return &MockClient_Create_Call{Call: _e.mock.On("Create", ctx, obj, packed)}
 }
@@
 func (_e *MockClient_Expecter) Delete(ctx interface{}, obj interface{}, opts ...interface{}) *MockClient_Delete_Call {
-  return &MockClient_Delete_Call{Call: _e.mock.On("Delete",
-    append([]interface{}{ctx, obj}, opts...)...)}
+  packed := make([]client.DeleteOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.DeleteOption))
+  }
+  return &MockClient_Delete_Call{Call: _e.mock.On("Delete", ctx, obj, packed)}
 }
@@
 func (_e *MockClient_Expecter) DeleteAllOf(ctx interface{}, obj interface{}, opts ...interface{}) *MockClient_DeleteAllOf_Call {
-  return &MockClient_DeleteAllOf_Call{Call: _e.mock.On("DeleteAllOf",
-    append([]interface{}{ctx, obj}, opts...)...)}
+  packed := make([]client.DeleteAllOfOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.DeleteAllOfOption))
+  }
+  return &MockClient_DeleteAllOf_Call{Call: _e.mock.On("DeleteAllOf", ctx, obj, packed)}
 }
@@
 func (_e *MockClient_Expecter) Get(ctx interface{}, key interface{}, obj interface{}, opts ...interface{}) *MockClient_Get_Call {
-  return &MockClient_Get_Call{Call: _e.mock.On("Get",
-    append([]interface{}{ctx, key, obj}, opts...)...)}
+  packed := make([]client.GetOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.GetOption))
+  }
+  return &MockClient_Get_Call{Call: _e.mock.On("Get", ctx, key, obj, packed)}
 }
@@
 func (_e *MockClient_Expecter) List(ctx interface{}, list interface__, opts ...interface{}) *MockClient_List_Call {
-  return &MockClient_List_Call{Call: _e.mock.On("List",
-    append([]interface{}{ctx, list}, opts...)...)}
+  packed := make([]client.ListOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.ListOption))
+  }
+  return &MockClient_List_Call{Call: _e.mock.On("List", ctx, list, packed)}
 }
@@
 func (_e *MockClient_Expecter) Patch(ctx interface{}, obj interface{}, patch interface{}, opts ...interface{}) *MockClient_Patch_Call {
-  return &MockClient_Patch_Call{Call: _e.mock.On("Patch",
-    append([]interface{}{ctx, obj, patch}, opts...)...)}
+  packed := make([]client.PatchOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.PatchOption))
+  }
+  return &MockClient_Patch_Call{Call: _e.mock.On("Patch", ctx, obj, patch, packed)}
 }
@@
 func (_e *MockClient_Expecter) Update(ctx interface{}, obj interface{}, opts ...interface{}) *MockClient_Update_Call {
-  return &MockClient_Update_Call{Call: _e.mock.On("Update",
-    append([]interface{}{ctx, obj}, opts...)...)}
+  packed := make([]client.UpdateOption, 0, len(opts))
+  for _, o := range opts {
+    packed = append(packed, o.(client.UpdateOption))
+  }
+  return &MockClient_Update_Call{Call: _e.mock.On("Update", ctx, obj, packed)}
 }

Add a compile-time interface check to catch future drift:

+// Compile-time check: ensure MockClient satisfies controller-runtime client.Client.
+var _ client.Client = (*MockClient)(nil)

To ensure the Apply signature matches your controller-runtime version, please confirm:

Also applies to: 148-151, 220-223, 293-295, 365-368, 563-565, 636-638, 904-906

Signed-off-by: aaronschweig <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/subroutine/authorization_model.go (1)

235-259: Handle empty SingularName to prevent invalid type identifiers.

Some Kubernetes API resources have an empty SingularName field, which would result in invalid type names in the generated schema (e.g., core_ instead of core_namespace).

Apply this diff to fall back to the resource Kind when SingularName is empty:

 func processAPIResourceIntoModel(resource metav1.APIResource, tpl *template.Template) (bytes.Buffer, error) {
 
 	scope := apiextensionsv1.ClusterScoped
 	if resource.Namespaced {
 		scope = apiextensionsv1.NamespaceScoped
 	}
 
 	group := "core"
 	if resource.Group != "" {
 		group = resource.Group
 	}
 
+	singular := resource.SingularName
+	if singular == "" {
+		singular = strings.ToLower(resource.Kind)
+	}
+
 	var buffer bytes.Buffer
 	err := tpl.Execute(&buffer, modelInput{
 		Name:     resource.Name,
 		Group:    strings.ReplaceAll(group, ".", "_"),
-		Singular: resource.SingularName,
+		Singular: singular,
 		Scope:    string(scope),
 	})
🧹 Nitpick comments (2)
internal/subroutine/authorization_model_test.go (1)

212-249: Consider reusing actual templates to reduce test brittleness.

The hardcoded module structure duplicates the logic from the main code. If the templates in authorization_model.go change, this test setup must be manually updated.

Consider importing and executing the actual templates to generate the expected module content dynamically, which would keep tests aligned with production code:

// Import the template from the main package or expose it for testing
buf, err := processAPIResourceIntoModel(metav1.APIResource{
    Name:         "namespaces",
    SingularName: "namespace",
    Namespaced:   false,
    Group:        "",
}, priviledgedTemplate)
assert.NoError(t, err)

moduleFiles = append(moduleFiles, language.ModuleFile{
    Name:     "internal_core_types_namespaces.fga",
    Contents: buf.String(),
})

This would eliminate the hardcoded string and ensure tests stay synchronized with template changes.

internal/subroutine/authorization_model.go (1)

149-149: Consider extracting magic string to a named constant.

The hard-coded string "orgs" represents a special store name that controls whether discovery is enabled.

Consider defining it as a constant for clarity:

const (
	schemaVersion = "1.2"
	organizationStoreName = "orgs"
)

Then use it:

-	if store.Name != "orgs" {
+	if store.Name != organizationStoreName {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2da4981 and ba45ee9.

📒 Files selected for processing (2)
  • internal/subroutine/authorization_model.go (5 hunks)
  • internal/subroutine/authorization_model_test.go (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-24T06:53:47.110Z
Learnt from: aaronschweig
PR: platform-mesh/security-operator#136
File: internal/subroutine/authorization_model.go:0-0
Timestamp: 2025-10-24T06:53:47.110Z
Learning: In internal/subroutine/authorization_model.go, the privileged template extends parent types (core_platform-mesh_io_account or core_namespace) with create/list/watch permissions for privileged resources. These permissions are defined at the parent level (e.g., create_{{ .Group }}_{{ .Name }}) rather than directly on the resource types, delegating control to the account or namespace scope.

Applied to files:

  • internal/subroutine/authorization_model.go
  • internal/subroutine/authorization_model_test.go
📚 Learning: 2025-10-24T06:20:22.771Z
Learnt from: aaronschweig
PR: platform-mesh/security-operator#136
File: internal/subroutine/authorization_model.go:74-81
Timestamp: 2025-10-24T06:20:22.771Z
Learning: In internal/subroutine/authorization_model.go, the NewDiscoveryClientFunc factory does not need to return an error because the *rest.Config passed to it is copied from an already validated manager config (a.mgr.GetLocalManager().GetConfig()), so the config is guaranteed to be valid at that point.

Applied to files:

  • internal/subroutine/authorization_model.go
🧬 Code graph analysis (1)
internal/subroutine/authorization_model_test.go (3)
internal/subroutine/authorization_model.go (1)
  • NewAuthorizationModelSubroutine (83-90)
internal/subroutine/mocks/mock_CTRLManager.go (1)
  • NewCTRLManager (27-37)
internal/subroutine/mocks/mock_DiscoveryInterface.go (1)
  • NewMockDiscoveryInterface (19-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: pipe / lint / lint
  • GitHub Check: pipe / dockerBuild / docker
🔇 Additional comments (2)
internal/subroutine/authorization_model.go (1)

198-216: Improved model comparison approach.

Using protobuf JSON serialization for comparison avoids DSL ordering issues and provides more reliable semantic equality checking.

internal/subroutine/authorization_model_test.go (1)

94-98: Fix compilation error: testing.T has no Context() method.

The testing.T type does not have a Context() method in standard Go. This will cause a compilation error.

Apply this diff to fix the error:

 func TestAuthorizationModelFinalize(t *testing.T) {
 	subroutine := subroutine.NewAuthorizationModelSubroutine(nil, nil, nil, nil, nil)
-	_, err := subroutine.Finalize(t.Context(), nil)
+	_, err := subroutine.Finalize(context.Background(), nil)
 	assert.Nil(t, err)
 }

Likely an incorrect or invalid review comment.

cmds:
- echo "https://openmfp:[email protected]" >> $HOME/.git-credentials
- git config --global url."https://${PLATFORM_MESH_TOKEN}@github.com/".insteadOf "https://github.com/"
- go test -count=1 ./... {{.ADDITIONAL_COMMAND_ARGS}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

count=1 is the default

@aaronschweig aaronschweig enabled auto-merge (squash) October 24, 2025 09:43
@aaronschweig aaronschweig merged commit de3cd53 into main Oct 24, 2025
10 of 11 checks passed
@aaronschweig aaronschweig deleted the feat/automatic-core-type-generation branch October 24, 2025 09:45
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
internal/subroutine/authorization_model.go (2)

235-259: Handle empty SingularName to avoid invalid type identifiers.

Some Kubernetes APIResources have an empty SingularName, which would produce invalid type names in the generated schema.

Apply this diff to add a fallback:

 func processAPIResourceIntoModel(resource metav1.APIResource, tpl *template.Template) (bytes.Buffer, error) {
 
 	scope := apiextensionsv1.ClusterScoped
 	if resource.Namespaced {
 		scope = apiextensionsv1.NamespaceScoped
 	}
 
 	group := "core"
 	if resource.Group != "" {
 		group = resource.Group
 	}
+
+	singular := resource.SingularName
+	if singular == "" {
+		singular = strings.ToLower(resource.Kind)
+	}
 
 	var buffer bytes.Buffer
 	err := tpl.Execute(&buffer, modelInput{
 		Name:     resource.Name,
 		Group:    strings.ReplaceAll(group, ".", "_"),
-		Singular: resource.SingularName,
+		Singular: singular,
 		Scope:    string(scope),
 	})

158-158: Ensure absolute path when rewriting API server URL.

url.JoinPath("clusters", ...) may produce a relative path. Preserve the base path or ensure a leading slash.

Apply this diff:

-parsed.Path, err = url.JoinPath("clusters", fmt.Sprintf("root:orgs:%s", store.Name))
+parsed.Path, err = url.JoinPath("/", "clusters", fmt.Sprintf("root:orgs:%s", store.Name))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a372cfb and 7bc6251.

📒 Files selected for processing (1)
  • internal/subroutine/authorization_model.go (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-24T06:53:47.110Z
Learnt from: aaronschweig
PR: platform-mesh/security-operator#136
File: internal/subroutine/authorization_model.go:0-0
Timestamp: 2025-10-24T06:53:47.110Z
Learning: In internal/subroutine/authorization_model.go, the privileged template extends parent types (core_platform-mesh_io_account or core_namespace) with create/list/watch permissions for privileged resources. These permissions are defined at the parent level (e.g., create_{{ .Group }}_{{ .Name }}) rather than directly on the resource types, delegating control to the account or namespace scope.

Applied to files:

  • internal/subroutine/authorization_model.go
📚 Learning: 2025-10-24T06:20:22.771Z
Learnt from: aaronschweig
PR: platform-mesh/security-operator#136
File: internal/subroutine/authorization_model.go:74-81
Timestamp: 2025-10-24T06:20:22.771Z
Learning: In internal/subroutine/authorization_model.go, the NewDiscoveryClientFunc factory does not need to return an error because the *rest.Config passed to it is copied from an already validated manager config (a.mgr.GetLocalManager().GetConfig()), so the config is guaranteed to be valid at that point.

Applied to files:

  • internal/subroutine/authorization_model.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pipe / dockerBuild / docker
🔇 Additional comments (4)
internal/subroutine/authorization_model.go (4)

4-9: LGTM! Import additions support template rendering and API discovery.

The new imports appropriately support the dynamic FGA schema generation from discovered Kubernetes resources.

Also applies to: 19-22


30-72: LGTM! Template structure correctly delegates create/list to parent types.

The privileged template appropriately extends parent types with scoped create/list/watch permissions, consistent with the architectural pattern confirmed in previous discussions.

Based on learnings


74-90: LGTM! Discovery client factory integration is clean.

The factory injection enables testability while keeping the production path simple with the validated config assumption.

Based on learnings


198-213: LGTM! JSON comparison is more robust than DSL string comparison.

Switching to JSON-based proto comparison correctly handles ordering differences and ensures semantic equivalence checking.


discoveryClient := a.newDiscoveryClientFunc(cfg)

coreModules, err := discoverAndRender(discoveryClient, modelTpl, groupVersions)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix compilation error: undefined variable modelTpl.

Line 168 references modelTpl which is not defined in this file, causing a compilation error.

You need to define a template for non-privileged core types. Add this definition in the var block (after line 36):

coreModelTemplate = template.Must(template.New("model").Parse(`module internal_core_types_{{ .Name }}

type {{ .Group }}_{{ .Singular }}
	relations
		define parent: [{{ if eq .Scope "Namespaced" }}core_namespace{{ else }}core_platform-mesh_io_account{{ end }}]
		define member: [role#assignee] or owner or member from parent
		define owner: [role#assignee] or owner from parent
		
		define get: member
		define update: member
		define delete: member
		define patch: member
		define watch: member

		define manage_iam_roles: owner
		define get_iam_roles: member
		define get_iam_users: member
`))

Then update line 168:

-coreModules, err := discoverAndRender(discoveryClient, modelTpl, groupVersions)
+coreModules, err := discoverAndRender(discoveryClient, coreModelTemplate, groupVersions)
🤖 Prompt for AI Agents
In internal/subroutine/authorization_model.go around lines 36 and 168, define a
new template variable in the var block after line 36 named coreModelTemplate
containing the provided template for non-privileged core types, then replace the
undefined modelTpl on line 168 with coreModelTemplate when calling
discoverAndRender; this ensures the template is defined and used correctly.

Comment on lines +261 to +286
func discoverAndRender(dc discovery.DiscoveryInterface, tpl *template.Template, groupVersions []string) ([]language.ModuleFile, error) {
var files []language.ModuleFile
for _, gv := range groupVersions {
resourceList, err := dc.ServerResourcesForGroupVersion(gv)
if err != nil {
return nil, fmt.Errorf("discover resources for %s: %w", gv, err)
}

for _, apiRes := range resourceList.APIResources {
if strings.Contains(apiRes.Name, "/") { // skip subresources
continue
}

buf, err := processAPIResourceIntoModel(apiRes, tpl)
if err != nil {
return nil, fmt.Errorf("process api resource %s in %s: %w", apiRes.Name, gv, err)
}

files = append(files, language.ModuleFile{
Name: fmt.Sprintf("internal_core_types_%s.fga", apiRes.Name),
Contents: buf.String(),
})
}
}
return files, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Include group in ModuleFile name to prevent collisions.

Resources with the same plural name in different API groups will overwrite each other's module files.

Modify discoverAndRender to return group information and update line 280:

-			files = append(files, language.ModuleFile{
-				Name:     fmt.Sprintf("internal_core_types_%s.fga", apiRes.Name),
-				Contents: buf.String(),
-			})
+			group := "core"
+			if apiRes.Group != "" {
+				group = strings.ReplaceAll(apiRes.Group, ".", "_")
+			}
+			files = append(files, language.ModuleFile{
+				Name:     fmt.Sprintf("internal_core_types_%s_%s.fga", group, apiRes.Name),
+				Contents: buf.String(),
+			})

Committable suggestion skipped: line range outside the PR's diff.

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.

extend the core schema to all by default available API's in a workspace

3 participants