Skip to content

feat(api): add SecurityGroup network policy resource (sdn.cozystack.io) - #2922

Merged
Aleksei Sviridkin (lexfrei) merged 24 commits into
mainfrom
feat/sdn-security-groups
Jun 30, 2026
Merged

feat(api): add SecurityGroup network policy resource (sdn.cozystack.io)#2922
Aleksei Sviridkin (lexfrei) merged 24 commits into
mainfrom
feat/sdn-security-groups

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Introduces a tenant-facing SecurityGroup resource in a new sdn.cozystack.io/v1alpha1 API group, served by the Cozystack aggregated API server as a 1:1 projection of a CiliumNetworkPolicy. A SecurityGroup attaches to one of the tenant's managed applications by reference (spec.targetRef: {apiGroup, kind, name}) and declares allow-list ingress/egress for that application. Tenants manage network policy for their own apps entirely through securitygroups.sdn.cozystack.io, without being granted any access to the cilium.io API group.

The translation is synchronous and stateless — the API server converts each SecurityGroup into a CiliumNetworkPolicy in the same namespace on write and back on read; there is no controller. The backing policy's endpointSelector is derived from targetRef (matched on the application's lineage labels apps.cozystack.io/application.{group,kind,name}), never copied from tenant input, so the selector is machine-generated and can only ever match the referenced application's own pods in the tenant's namespace. Policies owned by this API carry an sdn.cozystack.io/securitygroup marker label that the storage always re-asserts and hides from the view, so it only ever surfaces or mutates its own policies and never touches platform-managed network policies.

The value is self-service network policy on a Cozystack-native, RBAC-scoped surface, without exposing Cilium's version-coupled CRDs to tenants. Because the selector is derived from an authorized application reference rather than free-form tenant input, the RBAC boundary is structural, not selector-validated: a tenant cannot express network policy against arbitrary or platform-owned pods. The full design — selector derivation from lineage labels, the structural boundary, the additive-policy reality (a SecurityGroup widens a managed application's allowed traffic but cannot sever its platform-managed management plane, and equally cannot tighten below the platform baseline), the in-tree CiliumNetworkPolicy mirror that avoids a Cilium Go-module dependency, RBAC, and deferred extensions (raw-pod targeting, cluster scope, reusable groups) — is in pkg/apis/sdn/DESIGN.md.

Implements the attachment model from #1614.

Release note

feat(api): add the SecurityGroup resource (sdn.cozystack.io/v1alpha1) — a tenant-facing, namespace-scoped firewall that attaches to a managed application by reference and projects 1:1 onto a CiliumNetworkPolicy with a machine-derived endpoint selector, letting tenants manage their application's network policy without cilium.io access.

Summary by CodeRabbit

  • New Features
    • Added the SecurityGroup API resource (sdn.cozystack.io v1alpha1) with a namespaced projection backed by Cilium network policies.
    • Enabled the SDN APIService and wired SDN routing, scheme, and REST support.
    • Expanded RBAC to include SecurityGroup access and required Cilium policy permissions.
  • Documentation
    • Added a design document describing projection behavior and rule semantics.
  • Bug Fixes
    • Improved API rule validation coverage for missing list-type fields.
  • Tests
    • Added SDN round-trip, fuzzing, RBAC, and comprehensive REST create/get/list/update/delete/watch/resource-version tests.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces sdn.cozystack.io/v1alpha1 SecurityGroup as a new aggregated Kubernetes API resource. Each SecurityGroup is a synchronous 1:1 projection of a CiliumNetworkPolicy carrying an internal marker label (sdn.cozystack.io/securitygroup=true). The implementation spans API type definitions, generated deepcopy/conversion/defaults, a full REST storage layer with CRUD/Watch/Patch logic that injects and reasserts the marker label on every write, apiserver registration, Kubernetes APIService/RBAC manifests, tenant ClusterRole updates enabling access control, semantic validation of CIDR/port/protocol specs, and a comprehensive test suite covering projection semantics, force-update behavior, and watch event filtering.

Changes

SDN SecurityGroup API

Layer / File(s) Summary
SDN v1alpha1 API types and scheme registration
pkg/apis/sdn/DESIGN.md, pkg/apis/sdn/register.go, pkg/apis/sdn/v1alpha1/doc.go, pkg/apis/sdn/v1alpha1/register.go, pkg/apis/sdn/v1alpha1/zz_generated.*.go, pkg/apis/sdn/fuzzer/fuzzer.go, pkg/apis/sdn/install/install.go, pkg/apis/sdn/install/roundtrip_test.go, api/api-rules/cozystack_api_violation_exceptions.list
Defines SecurityGroup, SecurityGroupList, and rule/selector types (IngressRule, EgressRule, PortRule, PortProtocol, FQDNSelector); registers sdn.cozystack.io/v1alpha1 with SchemeBuilder, AddToScheme, and Resource helpers; adds generated deepcopy, conversion, and defaults stubs; includes design document describing the 1:1 aggregated-API projection semantics, marker-label visibility filtering, and semantic validation constraints; includes fuzzer callback, install helper, and round-trip test; adds list_type_missing API violation exceptions for list-typed rule fields.
CiliumNetworkPolicy mirror type and REST storage implementation
pkg/registry/sdn/securitygroup/cilium.go, pkg/registry/sdn/securitygroup/rest.go
Introduces in-tree CiliumNetworkPolicy mirror struct with AddToScheme and deepcopy methods; defines REST storage with marker-label constants and projection helpers that strip the marker from SecurityGroup views and inject it when writing to backing policies; implements Create (validate + marker injection + admit), Get (NotFound if unmarked), List (marker-enforced selector + field-selector parsing), Update (with force-create path + optimistic concurrency + reassert marker), Delete (marker ownership check), Watch (selector + field-selector filtering + event projection + bookmark support), ConvertToTable (name/age rendering), and Destroy (no-op).
SecurityGroup spec validation
pkg/registry/sdn/securitygroup/validate.go
Implements semantic validation rejecting schema-valid-but-incorrect specs: validates CIDR strings using net.ParseCIDR, validates port entries as numeric ranges (1–65535) or port names, validates protocols case-insensitively (TCP/UDP/SCTP/ANY); aggregates field errors and returns API Invalid error on validation failure.
REST storage tests: CRUD, admission, and marker label
pkg/registry/sdn/securitygroup/rest_test.go
Comprehensive test suite covering Create/Get/List/Update/Delete/ConvertToTable: spec-to-policy translation with marker injection/stripping, NotFound for unmarked policies, admission rejection (create/update/delete) with no persisted state, force-update semantics (no-clobber over unmarked, create-on-absent), label/annotation replacement on update, cluster-wide listing, metadata.name field-selector filtering, ConvertToTable output shape and HTTP 406 for unexpected types, resource-version concurrency (Conflict on stale RV, fallback when empty), marker label cannot be overridden on create/update, owner references and finalizers round-trip, and spec validation (invalid CIDR/port/protocol rejection, valid port/protocol acceptance).
REST storage tests: Watch event filtering
pkg/registry/sdn/securitygroup/rest_watch_test.go
Watch-specific test suite validating marker-label filtering (unmarked policies excluded from watch output, marker label not exposed on returned SecurityGroup), deleted event propagation through watch, SendInitialEvents bookmark with initial-events annotation, metadata.name field-selector filtering, and cluster-wide watch across namespaces; includes collectEvents helper for draining watch streams with timeout.
Apiserver scheme registration and SDN group installation
pkg/apiserver/apiserver.go, pkg/apiserver/scheme_test.go, pkg/cmd/server/start.go
Registers CiliumNetworkPolicy mirror types into manager Scheme via securitygroupstorage.AddToScheme(mgrScheme) in init(); installs sdn.GroupName API group with securitygroups namespaced REST storage via GenericAPIServer.InstallAPIGroup; extends legacy codec to include sdnv1alpha1.SchemeGroupVersion; adds scheme recognition tests for SecurityGroup/SecurityGroupList kinds and round-trip serialization tests using SDN fuzzer.
Kubernetes deployment: APIService, RBAC, and tenant ClusterRoles
packages/system/cozystack-api/templates/apiservice.yaml, packages/system/cozystack-api/templates/rbac.yaml, packages/system/cozystack-api/tests/rbac_test.yaml, packages/system/cozystack-basics/templates/clusterroles.yaml, packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
Registers v1alpha1.sdn.cozystack.io APIService pointing to cozystack-api service in cozy-system namespace with cert-manager CA injection annotation; grants cozystack-api service account full CRUD verbs on cilium.io ciliumnetworkpolicies (serving as backing storage for SecurityGroups); adds securitygroups resource with all-verbs access to cozy:tenant:base ClusterRole and read-only (get/list/watch) access to cozy:tenant:view:base ClusterRole for tenant access control; includes matching RBAC and clusterroles test assertions.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant APIServer as Aggregated APIServer
  participant REST
  participant Cilium as CiliumNetworkPolicy (backing)
  
  rect rgba(100, 149, 237, 0.5)
    Note over Client,Cilium: Create: SecurityGroup → CiliumNetworkPolicy + marker
    Client->>APIServer: POST /securitygroups (no marker in labels)
    APIServer->>REST: Create(SecurityGroup)
    REST->>REST: toPolicy() — inject sdn.cozystack.io/securitygroup=true
    REST->>Cilium: Create CiliumNetworkPolicy with marker label
    Cilium-->>REST: created policy
    REST->>REST: toSecurityGroup() — strip marker from view
    REST-->>APIServer: SecurityGroup (no marker exposed)
    APIServer-->>Client: 201 Created
  end
  
  rect rgba(144, 238, 144, 0.5)
    Note over Client,Cilium: Get & List: marker-enforced selector filtering
    Client->>APIServer: GET /securitygroups
    APIServer->>REST: List(opts)
    REST->>REST: Require marker label in selector
    REST->>Cilium: List (enforce marker-labeled selector)
    Cilium-->>REST: Marked policy only
    REST->>REST: Project to SecurityGroup
    REST-->>APIServer: SecurityGroupList
    APIServer-->>Client: items (marker stripped)
  end
  
  rect rgba(255, 165, 0, 0.5)
    Note over Client,Cilium: Update: reassert marker label on every write
    Client->>APIServer: PATCH /securitygroups/sg1 (user labels)
    APIServer->>REST: Update(sg1, spec)
    REST->>Cilium: Get current policy
    Cilium-->>REST: existing policy with marker
    REST->>REST: toPolicy() — copy user fields, reassert marker=true
    REST->>Cilium: Update CiliumNetworkPolicy
    Cilium-->>REST: updated
    REST-->>APIServer: SecurityGroup
    APIServer-->>Client: 200 OK (marker preserved)
  end
  
  rect rgba(173, 216, 230, 0.5)
    Note over Client,Cilium: Watch: filter by marker, project events
    Client->>APIServer: GET /securitygroups?watch=true
    APIServer->>REST: Watch(opts)
    REST->>Cilium: Watch CiliumNetworkPolicies (marker selector required)
    Cilium-->>REST: Added (unmarked platform policy)
    REST->>REST: isSecurityGroup()? → false, filter out
    Cilium-->>REST: Added (marked user policy)
    REST->>REST: isSecurityGroup()? → true, project to SecurityGroup
    REST-->>APIServer: watch.Added SecurityGroup
    APIServer-->>Client: watch event (no marker)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Suggested labels

area/testing

Suggested reviewers

  • kvaps
  • IvanHunters
  • sircthulhu
  • myasnikovdaniil
  • lllamnyp
  • androndo

Poem

🐇 Hoppity-hop, a new API blooms,
SecurityGroup guards Cilium's rooms!
Marker labels glow, projections align,
Watch streams flow clean down every vine.
No unmarked policy sneaks through the gate —
The rabbit's REST storage seals tenant fate! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: introducing a SecurityGroup network policy resource for the sdn.cozystack.io API group. It is specific, action-oriented, and accurately summarizes the primary contribution of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdn-security-groups

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.

@dosubot dosubot Bot added area/api Issues or PRs related to the cozystack-api aggregated API server kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/feature Categorizes issue or PR as related to a new feature labels Jun 15, 2026
@github-actions github-actions Bot added the size/XXL This PR changes 1000+ lines, ignoring generated files label Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
pkg/apis/sdn/v1alpha1/register.go (1)

16-21: ⚡ Quick win

Avoid duplicating API group literals across packages.

GroupName is duplicated here and in pkg/apis/sdn/register.go; centralizing it to the parent package avoids future contract drift between scheme registration layers.

Proposed refactor
 package v1alpha1
 
 import (
+	sdn "github.com/cozystack/cozystack/pkg/apis/sdn"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
@@
-const GroupName = "sdn.cozystack.io"
+const GroupName = sdn.GroupName
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/apis/sdn/v1alpha1/register.go` around lines 16 - 21, The GroupName
constant is duplicated between the v1alpha1 subpackage and its parent package,
which can lead to drift between the two definitions. Remove the GroupName
constant definition from this location (in the v1alpha1/register.go file) and
instead import GroupName from the parent sdn package. Then update the
SchemeGroupVersion variable to reference the imported GroupName from the parent
package to maintain a single source of truth for the API group identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/registry/sdn/securitygroup/rest.go`:
- Around line 501-520: The Watch method in the REST struct does not apply field
selector filtering from opts.FieldSelector, causing watch requests with field
selectors (like fieldSelector=metadata.name=sg-a) to stream all matching objects
instead of filtering by the specified field. Update the Watch method to parse
and apply opts.FieldSelector similar to how the List method handles it. Pass the
field selector to the watch operation by including it in the client.ListOptions
(specifically in the Raw metav1.ListOptions field) or by filtering the streamed
events appropriately to respect the field selector criteria.
- Around line 389-390: The CreateOptions at line 389 and UpdateOptions at line
488 are being created fresh, which drops important caller intent such as dryRun,
fieldManager, and fieldValidation. This causes semantic issues where dry-run
requests can become real writes. Instead of creating fresh options objects,
preserve the original request options in both locations—examine how the correct
pattern is implemented at lines 401 and 477, and apply that same approach to
both the Create call at line 389 and the Update call at line 488 to maintain
fieldManager attribution and respect dry-run semantics.
- Around line 514-524: The AllowWatchBookmarks field in the metav1.ListOptions
(within the Watch method call) is incorrectly being set to the sendInitialEvents
value, which ties these two independent Kubernetes API features together.
Instead, extract AllowWatchBookmarks independently from opts similar to how
SendInitialEvents is extracted at the beginning (check if
opts.AllowWatchBookmarks is not nil and dereference it), and use that separate
boolean value when setting AllowWatchBookmarks in the metav1.ListOptions
structure rather than reusing the sendInitialEvents variable.

---

Nitpick comments:
In `@pkg/apis/sdn/v1alpha1/register.go`:
- Around line 16-21: The GroupName constant is duplicated between the v1alpha1
subpackage and its parent package, which can lead to drift between the two
definitions. Remove the GroupName constant definition from this location (in the
v1alpha1/register.go file) and instead import GroupName from the parent sdn
package. Then update the SchemeGroupVersion variable to reference the imported
GroupName from the parent package to maintain a single source of truth for the
API group identifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0f0630d3-9a26-46c9-815d-04af3a715ed8

📥 Commits

Reviewing files that changed from the base of the PR and between 9d43e42 and 8ec416f.

⛔ Files ignored due to path filters (1)
  • pkg/generated/openapi/zz_generated.openapi.go is excluded by !**/generated/**
📒 Files selected for processing (24)
  • api/api-rules/cozystack_api_violation_exceptions.list
  • packages/system/cozystack-api/templates/apiservice.yaml
  • packages/system/cozystack-api/templates/rbac.yaml
  • packages/system/cozystack-api/tests/rbac_test.yaml
  • packages/system/cozystack-basics/templates/clusterroles.yaml
  • packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
  • pkg/apis/sdn/DESIGN.md
  • pkg/apis/sdn/fuzzer/fuzzer.go
  • pkg/apis/sdn/install/install.go
  • pkg/apis/sdn/install/roundtrip_test.go
  • pkg/apis/sdn/register.go
  • pkg/apis/sdn/v1alpha1/doc.go
  • pkg/apis/sdn/v1alpha1/register.go
  • pkg/apis/sdn/v1alpha1/securitygroup_types.go
  • pkg/apis/sdn/v1alpha1/zz_generated.conversion.go
  • pkg/apis/sdn/v1alpha1/zz_generated.deepcopy.go
  • pkg/apis/sdn/v1alpha1/zz_generated.defaults.go
  • pkg/apiserver/apiserver.go
  • pkg/apiserver/scheme_test.go
  • pkg/cmd/server/start.go
  • pkg/registry/sdn/securitygroup/cilium.go
  • pkg/registry/sdn/securitygroup/rest.go
  • pkg/registry/sdn/securitygroup/rest_test.go
  • pkg/registry/sdn/securitygroup/rest_watch_test.go

Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
Comment thread pkg/registry/sdn/securitygroup/rest.go
Comment thread pkg/registry/sdn/securitygroup/rest.go
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new SecurityGroup API resource that allows tenants to manage their own network policies in a namespace-scoped manner. By projecting these resources 1:1 onto CiliumNetworkPolicy objects via the aggregated API server, it provides a secure, self-service firewall interface without requiring tenants to have direct access to the underlying cilium.io API group. The implementation is stateless and synchronous, ensuring efficient policy management while maintaining strict isolation through marker-label scoping.

Highlights

  • New API Resource: Introduced the SecurityGroup resource in the sdn.cozystack.io/v1alpha1 API group to enable tenant-facing network policy management.
  • Stateless Projection: Implemented a synchronous, stateless projection layer that maps SecurityGroup resources 1:1 to CiliumNetworkPolicy objects.
  • Isolation and Scoping: Utilized marker-label scoping (sdn.cozystack.io/securitygroup) to ensure the API server only manages tenant-specific policies while ignoring platform-managed ones.
  • RBAC and Security: Updated RBAC configurations to allow the API server to manage CiliumNetworkPolicies on behalf of tenants without granting tenants direct access to the cilium.io API group.
  • Testing and Documentation: Added comprehensive test coverage, including roundtrip, RBAC, and fuzzer tests, alongside detailed design documentation.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: **/zz_generated.*.go (4)
    • pkg/apis/sdn/v1alpha1/zz_generated.conversion.go
    • pkg/apis/sdn/v1alpha1/zz_generated.deepcopy.go
    • pkg/apis/sdn/v1alpha1/zz_generated.defaults.go
    • pkg/generated/openapi/zz_generated.openapi.go
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the SecurityGroup resource under the new sdn.cozystack.io/v1alpha1 API group, implementing a namespace-scoped projection over CiliumNetworkPolicies. Feedback focuses on critical API server integration issues: using nsFrom(ctx) in listing and watching methods breaks cluster-wide operations, which can be resolved by using request.NamespaceValue(ctx). Additionally, OwnerReferences and Finalizers should be projected to preserve Kubernetes garbage collection and finalizer behaviors. The Port field should use intstr.IntOrString instead of string to support unquoted port numbers, and the custom Patch method can be removed as it is dead code under the generic apiserver PATCH handler. Finally, adding a unit test for cluster-wide listing is recommended.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
Comment on lines +273 to +276
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using nsFrom(ctx) in the List method forces a namespace to be present in the context. This completely breaks cluster-wide listing (e.g., kubectl get securitygroups -A), returning a 400 Bad Request instead. To support cluster-wide listing, retrieve the namespace using request.NamespaceValue(ctx), which correctly returns an empty string when listing across all namespaces.

Suggested change
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
ns := request.NamespaceValue(ctx)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 43e079e66List now uses request.NamespaceValue(ctx), which returns an empty namespace for a cluster-wide request, so kubectl get securitygroups -A lists across namespaces. Added TestListClusterWide.

Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
Comment on lines +502 to +505
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Similar to the List method, using nsFrom(ctx) in Watch breaks cluster-wide watching (e.g., kubectl get securitygroups -A -w). Use request.NamespaceValue(ctx) instead to allow watching across all namespaces.

Suggested change
ns, err := nsFrom(ctx)
if err != nil {
return nil, err
}
ns := request.NamespaceValue(ctx)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 43e079e66Watch now uses request.NamespaceValue(ctx) for the same reason, enabling cluster-wide watch. Added TestWatchClusterWide.

Comment on lines +74 to +82
ObjectMeta: metav1.ObjectMeta{
Name: np.Name,
Namespace: np.Namespace,
UID: np.UID,
ResourceVersion: np.ResourceVersion,
CreationTimestamp: np.CreationTimestamp,
Labels: stripInternal(np.Labels),
Annotations: np.Annotations,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Currently, OwnerReferences and Finalizers are not projected from the backing CiliumNetworkPolicy to the SecurityGroup resource. This breaks Kubernetes garbage collection and controller finalizers for SecurityGroup resources. Project them 1:1 to ensure correct behavior.

		ObjectMeta: metav1.ObjectMeta{
			Name:              np.Name,
			Namespace:         np.Namespace,
			UID:               np.UID,
			ResourceVersion:   np.ResourceVersion,
			CreationTimestamp: np.CreationTimestamp,
			Labels:            stripInternal(np.Labels),
			Annotations:       np.Annotations,
			OwnerReferences:   np.OwnerReferences,
			Finalizers:        np.Finalizers,
		},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 43e079e66policyToSecurityGroup now projects OwnerReferences and Finalizers from the backing CiliumNetworkPolicy. Covered by TestOwnerReferencesAndFinalizersRoundTrip.

Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
Comment on lines +114 to +124
if len(sg.Annotations) > 0 {
out.Annotations = make(map[string]string, len(sg.Annotations))
for k, v := range sg.Annotations {
out.Annotations[k] = v
}
} else {
out.Annotations = nil
}

out.Spec = sg.Spec.DeepCopy()
return &out

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Project OwnerReferences and Finalizers from the SecurityGroup resource to the backing CiliumNetworkPolicy during creation and updates to ensure garbage collection and finalizers are correctly propagated.

	if len(sg.Annotations) > 0 {
		out.Annotations = make(map[string]string, len(sg.Annotations))
		for k, v := range sg.Annotations {
			out.Annotations[k] = v
		}
	} else {
		out.Annotations = nil
	}

	out.OwnerReferences = sg.OwnerReferences
	out.Finalizers = sg.Finalizers

	out.Spec = sg.Spec.DeepCopy()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 43e079e66securityGroupToPolicy now projects OwnerReferences and Finalizers onto the backing policy with replace semantics (like labels/annotations), so garbage collection and finalizers work. Covered by TestOwnerReferencesAndFinalizersRoundTrip.


package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Import k8s.io/apimachinery/pkg/util/intstr to support the intstr.IntOrString type for the Port field.

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not adding this import — Port stays a string (see the PortProtocol thread), so intstr is not needed.

Comment on lines +89 to +98
// PortProtocol is a single port and protocol pair.
type PortProtocol struct {
// Port is the L4 port number as a string, or a named port. An empty value
// matches all ports.
Port string `json:"port,omitempty"`

// Protocol is the L4 protocol. One of TCP, UDP, SCTP or ANY. Defaults to ANY
// when empty.
Protocol string `json:"protocol,omitempty"`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using string for the Port field means that any unquoted port numbers (e.g., port: 5432) in the YAML input will fail to unmarshal, forcing users to always quote port numbers (e.g., port: "5432"). To align with standard Kubernetes conventions and prevent unmarshaling errors, consider using intstr.IntOrString instead.

Suggested change
// PortProtocol is a single port and protocol pair.
type PortProtocol struct {
// Port is the L4 port number as a string, or a named port. An empty value
// matches all ports.
Port string `json:"port,omitempty"`
// Protocol is the L4 protocol. One of TCP, UDP, SCTP or ANY. Defaults to ANY
// when empty.
Protocol string `json:"protocol,omitempty"`
}
// PortProtocol is a single port and protocol pair.
type PortProtocol struct {
// Port is the L4 port number as an integer or a named port. An empty value
// matches all ports.
Port intstr.IntOrString `json:"port,omitempty"`
// Protocol is the L4 protocol. One of TCP, UDP, SCTP or ANY. Defaults to ANY
// when empty.
Protocol string `json:"protocol,omitempty"`
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping Port as a string. The backing CiliumNetworkPolicy CRD defines toPorts.ports.port as a string, and this resource is a lossless 1:1 projection onto it — SecurityGroupSpec is reused verbatim as the backing policy's spec body. Switching to intstr.IntOrString would serialize an integer port into the CNP, which its schema rejects, so the projection would stop round-tripping. Quoting port numbers matches how Cilium policies are written.

Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
}

// Patch applies a patch to the CiliumNetworkPolicy backing the SecurityGroup.
func (r *REST) Patch(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Kubernetes apiserver, there is no rest.Patcher interface. PATCH requests are handled by the API server's generic handler by first calling Get to retrieve the resource, applying the patch in-memory, and then calling Update to persist the changes. Therefore, the custom Patch method on REST is dead code and will never be invoked by the API server at runtime. The marker label preservation is already fully handled by the Update path (via securityGroupToPolicy). You can safely remove the Patch method and its associated tests to simplify the codebase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — removed in 43e079e66. PATCH is served by the generic handler via Get+Update (rest.Patcher = Getter + Updater), and the Update path already re-asserts the marker label, so behavior is unchanged and the method was dead code.

if !sort.StringsAreSorted(names) || !reflect.DeepEqual(names, want) {
t.Fatalf("List names = %v, want sorted %v", names, want)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Add a unit test to verify that cluster-wide listing works correctly and does not regress.

}

func TestListClusterWide(t *testing.T) {
	r := newTestREST(t,
		&CiliumNetworkPolicy{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "sg-a",
				Namespace: "tenant-a",
				Labels:    map[string]string{sgLabelKey: sgLabelValue},
			},
		},
		&CiliumNetworkPolicy{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "sg-b",
				Namespace: "tenant-b",
				Labels:    map[string]string{sgLabelKey: sgLabelValue},
			},
		},
	)

	ctx := request.WithNamespace(context.Background(), metav1.NamespaceAll)
	out, err := r.List(ctx, &metainternal.ListOptions{})
	if err != nil {
		t.Fatalf("List returned error: %v", err)
	}
	list := out.(*sdnv1alpha1.SecurityGroupList)

	if len(list.Items) != 2 {
		t.Fatalf("expected 2 items, got %d", len(list.Items))
	}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 43e079e66TestListClusterWide and TestWatchClusterWide verify cross-namespace list/watch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/apis/sdn/DESIGN.md (1)

29-29: 💤 Low value

Use hyphenation for the compound adjective.

LanguageTool flagged: "project marked" should be hyphenated as "project-marked" when used as a compound adjective modifying CiliumNetworkPolicies.

✏️ Proposed fix
-3. Reads (`get`/`list`/`watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.
+3. Reads (`get`/`list`/`watch`) project-marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/apis/sdn/DESIGN.md` at line 29, The compound adjective "project marked"
in the line describing Reads operations should be hyphenated since it is used to
modify "CiliumNetworkPolicies". Change "project marked CiliumNetworkPolicies" to
"project-marked CiliumNetworkPolicies" to follow proper hyphenation rules for
compound adjectives.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/registry/sdn/securitygroup/rest.go`:
- Around line 504-516: In the watch call where metav1.ListOptions is being
constructed with the Raw field, the SendInitialEvents option from opts is not
being forwarded to the backing watch. Add the line SendInitialEvents:
opts.SendInitialEvents, to the Raw metav1.ListOptions struct (alongside the
existing AllowWatchBookmarks, Watch, and ResourceVersion fields) to ensure that
when initial events are requested by the client, the backing watch is properly
configured to handle them according to Kubernetes watch semantics.

---

Nitpick comments:
In `@pkg/apis/sdn/DESIGN.md`:
- Line 29: The compound adjective "project marked" in the line describing Reads
operations should be hyphenated since it is used to modify
"CiliumNetworkPolicies". Change "project marked CiliumNetworkPolicies" to
"project-marked CiliumNetworkPolicies" to follow proper hyphenation rules for
compound adjectives.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 95e1e375-8e2e-4e8c-ad59-8aa4c0e1cefc

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec416f and c595f22.

⛔ Files ignored due to path filters (1)
  • pkg/generated/openapi/zz_generated.openapi.go is excluded by !**/generated/**
📒 Files selected for processing (24)
  • api/api-rules/cozystack_api_violation_exceptions.list
  • packages/system/cozystack-api/templates/apiservice.yaml
  • packages/system/cozystack-api/templates/rbac.yaml
  • packages/system/cozystack-api/tests/rbac_test.yaml
  • packages/system/cozystack-basics/templates/clusterroles.yaml
  • packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
  • pkg/apis/sdn/DESIGN.md
  • pkg/apis/sdn/fuzzer/fuzzer.go
  • pkg/apis/sdn/install/install.go
  • pkg/apis/sdn/install/roundtrip_test.go
  • pkg/apis/sdn/register.go
  • pkg/apis/sdn/v1alpha1/doc.go
  • pkg/apis/sdn/v1alpha1/register.go
  • pkg/apis/sdn/v1alpha1/securitygroup_types.go
  • pkg/apis/sdn/v1alpha1/zz_generated.conversion.go
  • pkg/apis/sdn/v1alpha1/zz_generated.deepcopy.go
  • pkg/apis/sdn/v1alpha1/zz_generated.defaults.go
  • pkg/apiserver/apiserver.go
  • pkg/apiserver/scheme_test.go
  • pkg/cmd/server/start.go
  • pkg/registry/sdn/securitygroup/cilium.go
  • pkg/registry/sdn/securitygroup/rest.go
  • pkg/registry/sdn/securitygroup/rest_test.go
  • pkg/registry/sdn/securitygroup/rest_watch_test.go
✅ Files skipped from review due to trivial changes (3)
  • pkg/apis/sdn/install/roundtrip_test.go
  • pkg/apis/sdn/v1alpha1/doc.go
  • pkg/apis/sdn/v1alpha1/zz_generated.defaults.go
🚧 Files skipped from review as they are similar to previous changes (15)
  • pkg/apis/sdn/fuzzer/fuzzer.go
  • packages/system/cozystack-api/templates/rbac.yaml
  • pkg/apis/sdn/register.go
  • packages/system/cozystack-api/templates/apiservice.yaml
  • packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
  • pkg/cmd/server/start.go
  • pkg/apis/sdn/v1alpha1/securitygroup_types.go
  • pkg/apis/sdn/v1alpha1/zz_generated.conversion.go
  • pkg/registry/sdn/securitygroup/cilium.go
  • api/api-rules/cozystack_api_violation_exceptions.list
  • pkg/apiserver/scheme_test.go
  • pkg/apis/sdn/v1alpha1/register.go
  • pkg/apiserver/apiserver.go
  • pkg/apis/sdn/v1alpha1/zz_generated.deepcopy.go
  • pkg/registry/sdn/securitygroup/rest_test.go

Comment thread pkg/registry/sdn/securitygroup/rest.go
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the feat/sdn-security-groups branch 2 times, most recently from 36cf376 to 20e3676 Compare June 16, 2026 12:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
pkg/apis/sdn/DESIGN.md (1)

29-29: 💤 Low value

Improve sentence structure for clarity.

The phrase "Reads (get/list/watch) project marked CiliumNetworkPolicies back into SecurityGroups" could be clearer. Consider restructuring to explicitly connect the read operations with the projection action.

💭 Proposed revision
-3. Reads (`get`/`list`/`watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.
+3. Reads (`get`, `list`, `watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.

or alternatively:

-3. Reads (`get`/`list`/`watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.
+3. Read operations (`get`, `list`, `watch`) project marked CiliumNetworkPolicies back into SecurityGroups. The marker label is hidden from the SecurityGroup view.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/apis/sdn/DESIGN.md` at line 29, The sentence beginning with "Reads
(`get`/`list`/`watch`) project marked CiliumNetworkPolicies back into
SecurityGroups" is unclear because it doesn't explicitly connect the read
operations with the projection action. Restructure this sentence to make the
relationship between the read operations (get, list, watch) and the projection
of marked CiliumNetworkPolicies into SecurityGroups more explicit and
grammatically clear. Ensure the revised sentence flows logically and maintains
the context that the marker label is hidden from the SecurityGroup view.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/registry/sdn/securitygroup/rest_test.go`:
- Around line 124-233: Add two new regression test functions to validate
namespace and name identity boundaries. First, add a test function (e.g.,
TestCreateRejectsNamespaceMismatch) that verifies the Create method rejects a
SecurityGroup when metadata.namespace differs from the request namespace
(testNamespace). Second, add a test function (e.g.,
TestUpdateRejectsNameMismatch) that verifies the Update method rejects an update
when the SecurityGroup metadata.name differs from the URL name parameter passed
to the Update call. Both tests should assert that appropriate validation errors
are returned to prevent security boundary violations. Place these new test
functions after the existing test functions in the file to complete the
regression test coverage for these critical security contracts.

In `@pkg/registry/sdn/securitygroup/rest.go`:
- Around line 251-280: The Create method does not enforce that the SecurityGroup
being created belongs to the request's namespace, allowing a caller to post to
one namespace endpoint and persist to a different namespace. After the type
assertion check for the SecurityGroup object (the `in` variable), explicitly
bind the SecurityGroup's namespace to the namespace extracted from the request
context before any validation or persistence occurs. This ensures the object can
only be created in the intended namespace regardless of what namespace value was
provided in the request body.
- Around line 371-426: The Update function does not validate that the
SecurityGroup object's metadata.name matches the URL name parameter, which could
allow writing unintended objects in force-create flows. After casting the object
to sdnv1alpha1.SecurityGroup in the `in` variable and before calling
validateSecurityGroup, add a validation check that compares the SecurityGroup's
name field with the name parameter passed to the Update function, returning a
NewBadRequest or appropriate error if they do not match.
- Around line 608-613: The RV filtering logic at lines 608-613 in the watch
handler is incorrectly dropping ADDED events that are part of the initial replay
when sendInitialEvents=true. These replayed ADDED events intentionally have
ResourceVersion values less than or equal to the starting RV. Modify the
condition that guards the RV filtering check to only apply this filter to live
ADDED events that arrive after the initial replay completes, not to the replayed
events during the initial phase. You will need to track or detect when the
initial replay phase ends (typically marked by receiving a Bookmark event or
reaching the end of the backlog) and only apply the parseErr and objRV
comparison when processing post-replay events.

---

Nitpick comments:
In `@pkg/apis/sdn/DESIGN.md`:
- Line 29: The sentence beginning with "Reads (`get`/`list`/`watch`) project
marked CiliumNetworkPolicies back into SecurityGroups" is unclear because it
doesn't explicitly connect the read operations with the projection action.
Restructure this sentence to make the relationship between the read operations
(get, list, watch) and the projection of marked CiliumNetworkPolicies into
SecurityGroups more explicit and grammatically clear. Ensure the revised
sentence flows logically and maintains the context that the marker label is
hidden from the SecurityGroup view.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 728e225d-6c8d-475e-a65e-9ff0f2d83379

📥 Commits

Reviewing files that changed from the base of the PR and between 36cf376 and 20e3676.

📒 Files selected for processing (14)
  • packages/system/cozystack-api/templates/apiservice.yaml
  • packages/system/cozystack-api/templates/rbac.yaml
  • packages/system/cozystack-api/tests/rbac_test.yaml
  • packages/system/cozystack-basics/templates/clusterroles.yaml
  • packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
  • pkg/apis/sdn/DESIGN.md
  • pkg/apiserver/apiserver.go
  • pkg/apiserver/scheme_test.go
  • pkg/cmd/server/start.go
  • pkg/registry/sdn/securitygroup/cilium.go
  • pkg/registry/sdn/securitygroup/rest.go
  • pkg/registry/sdn/securitygroup/rest_test.go
  • pkg/registry/sdn/securitygroup/rest_watch_test.go
  • pkg/registry/sdn/securitygroup/validate.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/system/cozystack-basics/tests/clusterroles-options_test.yaml
  • pkg/apiserver/scheme_test.go
  • packages/system/cozystack-api/tests/rbac_test.yaml
  • packages/system/cozystack-api/templates/apiservice.yaml
  • packages/system/cozystack-api/templates/rbac.yaml
  • pkg/apiserver/apiserver.go
  • pkg/registry/sdn/securitygroup/cilium.go
  • packages/system/cozystack-basics/templates/clusterroles.yaml
  • pkg/registry/sdn/securitygroup/rest_watch_test.go

Comment thread pkg/registry/sdn/securitygroup/rest_test.go
Comment thread pkg/registry/sdn/securitygroup/rest.go
Comment thread pkg/registry/sdn/securitygroup/rest.go
Comment thread pkg/registry/sdn/securitygroup/rest.go Outdated
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the feat/sdn-security-groups branch 5 times, most recently from 832a1e6 to d1c0bf8 Compare June 16, 2026 13:30
Introduce the sdn.cozystack.io/v1alpha1 API group with the SecurityGroup
resource. SecurityGroup is a namespace-scoped, tenant-facing firewall
object whose spec mirrors the subset of the CiliumNetworkPolicy rule that
the abstraction exposes (endpointSelector, ingress, egress, ports, CIDR,
FQDN), so the resource can be projected 1:1 onto a CiliumNetworkPolicy
without granting tenants direct access to the cilium.io API group.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
The aggregated API server translates each SecurityGroup into a single
CiliumNetworkPolicy in the same namespace and back. Policies owned by the
SecurityGroup API are marked with the sdn.cozystack.io/securitygroup label,
which the storage always re-asserts on every write and hides from the
SecurityGroup view, so it only ever surfaces or mutates its own policies,
leaves platform-managed policies untouched, and cannot be orphaned by a
tenant overwriting the marker through spec labels. Mutating verbs run the
genericapiserver admission hooks, and updates replace labels/annotations
rather than merging them, matching Kubernetes PUT semantics.

To avoid importing the full Cilium module — which pins a Kubernetes
version incompatible with this project's apimachinery fork — the backing
object is a minimal in-tree CiliumNetworkPolicy mirror that reuses
SecurityGroupSpec as its spec body, making the projection a near-identity
translation in both directions.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
Install the sdn.cozystack.io API group and serve the securitygroups
resource from the SecurityGroup REST storage, register the static
SecurityGroup kinds, add the group to the server codec, and register the
in-tree CiliumNetworkPolicy mirror with the client scheme.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
Register the v1alpha1.sdn.cozystack.io aggregated APIService, grant the
cozystack-api ServiceAccount full access to CiliumNetworkPolicy (the
backing object for SecurityGroup), and let tenants self-service their
SecurityGroups: full access in the base tenant role and read access in
the view role, mirroring how apps.cozystack.io resources are exposed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Co-authored-by: Timofei Larkin <lllamnyp@gmail.com>
@lllamnyp

Copy link
Copy Markdown
Member

Where this should head: security groups as attachable membership groups

First — the targetRef rework is a real improvement and the reasoning behind it is sound. Deriving the backing endpointSelector from an application's lineage labels instead of a tenant-authored selector is exactly the right instinct: it makes the selection machine-generated, namespace-scoped, and impossible to point at arbitrary or platform-owned pods. The projection plumbing, the marker-label scoping, the synchronous validation, the watch handling, and the RBAC wiring are all groundwork that survives whatever we build next. None of that is in question.

I want to use this PR to align on the destination, because one API decision here — targetRef living in the SecurityGroup spec — is the thing I'd like us to reconsider before it sets as the public contract.

The destination: a configurable firewall, not a fixed one

The end state we're building toward is a tenant-configurable firewall. Today the per-tenant baseline (allow-internal-communication, allow-external-communication, the *-ingress/*-egress CCNPs in packages/apps/tenant/templates/networkpolicy.yaml) blanket-allows essentially all intra-namespace and outbound traffic. The plan is to shrink that baseline to the bare minimum the platform needs to keep managed applications running — DNS, apiserver, monitoring scrape, each app's own internal flows — and then make tenant namespaces default-deny, with tenants opening the rest themselves through SecurityGroups.

That is what gives this feature a reason to exist. Under today's allow-all baseline a SecurityGroup can only add allowances on top of an already-open namespace, so it cannot actually restrict anything — ingress: [] does not deny, and narrowing a destination does not narrow anything. That is fine as a stepping stone, but it means the SecurityGroup API we settle on now should be the one that still makes sense in the default-deny world, where the common rule a tenant writes is "my web app may reach my db app."

Why attachment + membership beats a targetRef field

In that world the rules tenants reach for are app-to-app, and quickly after, group-to-group. The vocabulary should be: a SecurityGroup targets applications, and its peers are applications, other SecurityGroups, CIDRs, and FQDNs. Entities (world, cluster, kube-apiserver) stay platform-managed and are deliberately not tenant-expressible — external egress is expressed as FQDNs/CIDRs.

The cleanest way to deliver app-to-app and group-to-group references is to make a SecurityGroup a real membership group rather than a pointer to one application:

  • A SecurityGroup has a stable identity of its own — a membership label, e.g. securitygroup.sdn.cozystack.io/<name>: "".
  • Attaching a SecurityGroup to an application stamps that label on the application's pods. The backing CiliumNetworkPolicy's endpointSelector matches the SecurityGroup's own membership label, decoupled from any single application — so one SecurityGroup can attach to several applications.
  • fromApp/toApp resolve to the application's lineage labels (the stable identity this PR already relies on).
  • fromSG/toSG resolve to the other SecurityGroup's membership label key directly.

That last point is the payoff. With a real membership label, fromSG is trivial and, crucially, live: it references a label the dataplane resolves dynamically, so re-attaching a group to a different application just works, with no stale rules. If instead a SecurityGroup only carries a targetRef, a fromSG reference has to be dereferenced to the target application and frozen into the policy at write time — which goes stale the moment the referenced group is re-targeted, and cannot even be reconstructed faithfully on read. Live group-to-group references and a membership model are the same decision; you cannot get the former without the latter.

The cost, and why it is smaller than it looks

The honest trade is that membership requires a stateful piece: a controller that maintains the labels as applications and attachments come and go, with finalizers for clean detach and a short eventual-consistency window. That is the cost #1614 weighed when it specced this same model (membership label per group, a relabeling controller, fromSecurityGroups/toSecurityGroups).

What makes that controller cheap to build is that the identity it needs already lands on every pod for free. The lineage webhook (internal/lineagecontrollerwebhook) already stamps each managed-application pod with its application identity (apps.cozystack.io/application.{group,kind,name}) at admission. A dedicated SecurityGroup controller can watch pod create/update events, read those identity labels that are already present, look up which SecurityGroups are attached to that application, and add or remove the membership labels accordingly — reconciling existing pods when an attachment changes. It never has to walk the ownership graph itself, and it adds no responsibility to the webhook: the webhook keeps doing exactly what it does today, and the new controller simply consumes its output. The relabeling controller that made this model look heavy in #1614 is a thin layer on top of identity that already exists.

One thing this also makes clean: because tenants have no direct access to Kubernetes primitives — only apps.cozystack.io resources — the membership label is written exclusively by the platform, never by tenants, and there are no raw tenant pods to reason about. Applications are the whole tenant-facing surface, which is exactly why attaching to applications (and gating that attachment by permission on the application) is the right authorization model.

What I'm asking

Let's converge on the membership-group model — SecurityGroups attached to applications, peers expressed as fromApp/toApp/fromSG/toSG/CIDR/FQDN — before targetRef becomes the API tenants depend on. The projection, validation, marker-scoping, and RBAC work in this PR carries forward; the change is moving "what this SecurityGroup applies to" out of a spec field and into an attachment backed by a membership label. Happy to write up the attachment surface and the controller/webhook split in detail so we can scope it.

@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as draft June 23, 2026 11:34
Give each SecurityGroup its own membership label
(securitygroup.sdn.cozystack.io/<name>) and select it from the backing
CiliumNetworkPolicy, instead of deriving the endpointSelector from a single
app's lineage labels. spec.targetRef becomes spec.attachments, so one
SecurityGroup can apply to several applications at once.

Peers move from free-form fromEndpoints/toEndpoints selectors to
fromApp/toApp (resolved to lineage labels) and fromSG/toSG (resolved to
another group's membership label, a reference the Cilium dataplane resolves
live). Attachments are persisted in a storage-owned CiliumNetworkPolicy
annotation, round-tripped and hidden from the SecurityGroup view like the
marker label. Validation now covers attachments and app/SG peers and rejects
names that collide with reserved Cilium entities.

The membership labels are maintained by a separate controller added in a
follow-up commit; this change covers the API types and the
aggregated-apiserver projection only.

BREAKING CHANGE: SecurityGroup spec.targetRef is replaced by
spec.attachments, and ingress/egress peers use fromApp/fromSG/toApp/toSG
instead of fromEndpoints/toEndpoints. The feature is unreleased, so no
migration is required.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…tyGroups

Add a controller that keeps each SecurityGroup's membership label
(securitygroup.sdn.cozystack.io/<name>) in sync with its attachments: it
stamps the label onto the pods of every attached application and removes it
when an application is detached or the SecurityGroup is deleted. The backing
CiliumNetworkPolicy's endpointSelector matches that label, so this is what
makes a SecurityGroup apply to its members.

The controller watches the marked backing policies and managed-app pods. It
resolves each attachment through the lineage labels the lineage webhook
already stamps — never the attachment list directly — and only ever labels
pods in the SecurityGroup's own namespace, using single-key merge patches so
it cannot clobber another group's label or a pod's lineage labels. A finalizer
on the backing policy guarantees the labels are stripped before the policy is
removed. Informer caches are bounded to managed pods and SecurityGroup-owned
policies.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Add the securitygroup-controller Helm package (Deployment, ServiceAccount,
ClusterRole and binding, image build) and register it as a default system
package so it ships with the platform.

The ClusterRole grants cluster-wide pod patch — the controller stamps the
SecurityGroup membership label on managed-app pods across dynamically-created
tenant namespaces, so the grant cannot be namespace-scoped — plus read and
finalizer patch on the backing CiliumNetworkPolicies, and leader-election
leases. Tenants gain no new permissions; the membership label is written
exclusively by this platform controller. helm-unittest pins the pod-patch and
ciliumnetworkpolicies grants so a dropped verb fails the suite rather than
silently failing closed at runtime.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Rewrite the SecurityGroup design for the membership model: each group's own
identity label, attachments stored in a backing-policy annotation, the
REST/controller split, live fromSG/toSG references, the controller's hardening
invariants and the eventual-consistency window contract. Spell out that the
default-deny baseline flip and the membership admission webhook are out of
scope and why, and that membership relocates rather than fixes the "tenant
firewalls its own managed application" concern.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Rewrite the SecurityGroup e2e for the membership model: assert the backing
CiliumNetworkPolicy's endpointSelector is the group's own membership label,
that attachments ride a storage-owned annotation hidden from the view, and
that fromApp/fromSG peers project to lineage- and membership-label endpoint
selectors the real cilium.io/v2 CRD accepts. A second test creates a
managed-labeled pod and asserts the securitygroup-controller stamps the
membership label on attach and clears it on detach.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — the membership-group model is the right shape, and it's well built

This is exactly the direction we wanted, implemented carefully. A few things I want to call out as done right:

  • The API reads the way tenants think. spec.attachments lets one SecurityGroup apply to several applications, and peers are fromApp/toApp and fromSG/toSG instead of raw selectors. App-to-app and group-to-group are now first-class.
  • fromSG/toSG are live. Because each SecurityGroup owns a membership label (securitygroup.sdn.cozystack.io/<name>) and references resolve to the other group's membership key, a group-to-group reference follows membership as attachments change — no stale, frozen selectors. That liveness is the whole reason to carry the membership label, and it works.
  • The controller's containment is right. It resolves each attachment through the lineage labels rather than trusting the attachment list to pick pods directly, lists pods only within the SecurityGroup's own namespace, and writes single-key merge patches. So a tenant-driven, cluster-wide label writer can't reach pods outside the tenant's namespace or clobber another group's label. The finalizer strips membership before the backing policy is removed, caches are bounded to managed pods and marked policies, and the periodic resync backstops a missed pod event.
  • Validation closes the projection edge cases. Rejecting a SecurityGroup name that can't form a valid label key — with a clean metadata.name error instead of a confusing backing-policy write failure — and rejecting peer names that collide with reserved Cilium entities are both the right calls.

One small thing I'd like fixed in this PR, then two notes for later.

Please fix before merge

securitygroup_types.go still documents an empty ingress/egress list as denying all traffic to the group's member pods:

An empty list denies all ingress to the group's member pods.

Under the current per-tenant baseline (allow-internal-communication and friends blanket-allow the namespace) that isn't true yet — an empty rule list adds no allowance but denies nothing, because the policy can only widen on top of the baseline. Until the baseline moves to default-deny, this comment promises enforcement the feature doesn't provide. Please qualify it (e.g. "once the namespace baseline is default-deny, an empty list denies all ingress to the member pods; under today's allow-all baseline it has no effect"). Same for the egress comment.

Notes for follow-up (not blocking)

  • Membership is applied after a pod starts, not at admission. That's the deliberate consequence of using a controller rather than extending the lineage webhook, and it's harmless today because the model is allow-only over an allow-all baseline. Once the baseline goes default-deny, a freshly-scaled pod will have a brief window before the controller stamps its membership label and its SecurityGroup-granted allows take effect — long enough to blip a pod's own startup egress. Worth documenting as a known property, and revisiting (admission-time stamping) only if it actually bites.
  • The feature stays inert until the baseline tightens. This PR is the mechanism; the separate work of shrinking the per-tenant baseline to the minimum the platform needs and making tenant namespaces default-deny is what gives these rules teeth. That's the expected sequence, just naming it so the inertness isn't mistaken for a defect in this PR.

Approving on the strength of the design and implementation; the only in-tree ask is the doc qualification above.

@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review June 29, 2026 11:54
@dosubot dosubot Bot added the area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) label Jun 29, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the SecurityGroup API, a namespaced projection over CiliumNetworkPolicy, and the securitygroup-controller to manage pod membership labels. The implementation includes the necessary API types, REST storage, and controller logic to ensure that pods of attached applications are correctly labeled for network policy enforcement. I have identified a high-severity issue regarding an invalid Go version in the Dockerfile and provided feedback on improving observability by logging errors in the reconciler.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@@ -0,0 +1,24 @@
FROM golang:1.26-alpine AS builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The Go version 1.26 specified in the FROM instruction does not exist yet. This will cause the Docker build to fail. Please use a recent, valid Go version, for example 1.22-alpine.

FROM golang:1.22-alpine AS builder

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

golang:1.26 is a real, published image — including the -alpine variant used here (both are on Docker Hub, and the cozystack builder images already build on golang:1.26). The build pulls it successfully; the "does not exist" note is stale training data. Keeping golang:1.26-alpine.

Comment on lines +114 to +123
func decodeAttachments(s string) []sdnv1alpha1.ApplicationReference {
if s == "" {
return nil
}
var refs []sdnv1alpha1.ApplicationReference
if err := json.Unmarshal([]byte(s), &refs); err != nil {
return nil
}
return refs
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

While it's reasonable to treat a malformed attachments annotation as empty to prevent the reconciler from crashing, swallowing the error silently can make debugging difficult. It would be beneficial for observability to log this error.

Consider changing the function signature to decodeAttachments(ctx context.Context, s string) and logging the error. You will also need to update the call sites in Reconcile and mapPodToSGs to pass the context.

// decodeAttachments parses the attachments annotation. A missing or malformed
// value yields nil.
func decodeAttachments(ctx context.Context, s string) []sdnv1alpha1.ApplicationReference {
	if s == "" {
		return nil
	}
	var refs []sdnv1alpha1.ApplicationReference
	if err := json.Unmarshal([]byte(s), &refs); err != nil {
		log.FromContext(ctx).Error(err, "failed to decode attachments annotation", "value", s)
		return nil
	}
	return refs
}
References
  1. Handle errors explicitly. Discarding meaningful errors with _ is a bug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 881591d. decodeAttachments now takes a context and logs the error via log.FromContext before falling back to empty, so a malformed annotation is still tolerated (the reconciler stays resilient) but no longer fails silently. Covered by TestDecodeAttachments (empty / malformed / valid).

Comment on lines +290 to +292
if err := r.List(ctx, cnps, client.InNamespace(pod.Namespace), client.MatchingLabels{sgLabelKey: sgLabelValue}); err != nil {
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

In mapPodToSGs, an error from r.List is swallowed. While returning nil is the correct way to signal no reconciliation requests, logging the error would improve observability, especially for transient API errors that might cause a pod update to be missed until the next periodic resync.

Suggested change
if err := r.List(ctx, cnps, client.InNamespace(pod.Namespace), client.MatchingLabels{sgLabelKey: sgLabelValue}); err != nil {
return nil
}
if err := r.List(ctx, cnps, client.InNamespace(pod.Namespace), client.MatchingLabels{sgLabelKey: sgLabelValue}); err != nil {
log.FromContext(ctx).Error(err, "failed to list CiliumNetworkPolicies for pod mapping")
return nil
}
References
  1. Handle errors explicitly. Discarding meaningful errors with _ is a bug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 881591d. The swallowed List error in mapPodToSGs is now logged via log.FromContext before returning no requests, so a transient API error during pod→SecurityGroup mapping is visible instead of silently deferring the pod until the next resync.

…d target

The securitygroup-controller package has a working `make image` target, but it
was never wired into the top-level Makefile `build:` list, so CI never builds or
pushes the controller image. The shipped values.yaml therefore keeps its
`securitygroup-controller:v0.0.0` placeholder and the controller Deployment
lands in ImagePullBackOff, since that image tag does not exist in the registry.
Add the missing build line next to the other first-party controllers so CI
builds, pushes, and digest-pins the image like every other controller.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…n the API

Two watch-proxy defects in the SecurityGroup REST storage:

- watch.Error events from the backing CiliumNetworkPolicy watch (e.g. a 410 Gone
  for an expired resourceVersion) carry a *metav1.Status, not a
  CiliumNetworkPolicy. The CNP type assertion dropped them, so the client saw a
  cleanly closed stream and never performed the required relist. Forward error
  events verbatim before the type assertion.

- The DELETED-event bypass the label selector needs (a policy whose labels mutate
  out of the selector is deleted carrying non-matching labels, so dropping it
  would strand cached clients) was wrongly applied to the field selector too.
  Name and namespace are immutable, so a field-selected watch (metadata.name=sg-a)
  received other objects' deletions and could corrupt filtered client caches.
  Apply the field filter to every event type.

Both paths are covered by new tests driving a fake backing watch.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The reconciler intentionally treats a malformed attachments annotation and a
failed pod-mapping List as empty / no-op to stay resilient, but it swallowed
both errors silently. That hides real defects: a bad annotation writer, or a
transient API error that drops a pod update until the next periodic resync with
no trace. Log both while preserving the resilient return behavior.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
…s not deny

The Ingress/Egress field docs claimed "an empty list denies all ingress/egress",
which is a false security guarantee. A SecurityGroup projects onto a
CiliumNetworkPolicy that only ADDS allow rules, and Cilium policies are additive
over the tenant's blanket-allow baseline, so an empty list does not isolate a
pod — effective connectivity stays open. Deny / default-deny enforcement depends
on the default-deny baseline, which is separate future work.

Rewrite the API field docs to the accurate semantics, regenerate the OpenAPI so
the served schema description matches, and fix the one DESIGN.md sentence that
still asserted "denies all traffic" (it already contradicted its own safety
section, which correctly states the API cannot deny anything today).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A SecurityGroup with attachments but no ingress or egress rules exists
only to stamp its membership label on the attached applications' pods, so
other SecurityGroups can reference it as a peer. Its backing
CiliumNetworkPolicy carried only an endpointSelector, which the
cilium.io/v2 CRD rejects synchronously: the spec anyOf requires at least
one of ingress/ingressDeny/egress/egressDeny, so a selector-only policy
fails with "spec.ingress: Required value". A membership-only group
could therefore never be created.

Always serialize the backing policy's ingress section, emitting an empty
list when the group has no ingress rules. An empty list is the CRD's
documented no-op ("if omitted or empty, this rule does not apply at
ingress") and enableDefaultDeny defaults to false for a direction with
no rules, so the policy is schema-valid yet datapath-inert: it neither
allows nor denies, matching the additive, non-isolating semantics of an
empty rule list. The projection guarantees the slice is non-nil so it
never marshals to null, which the CRD (no nullable fields) would also
reject. The empty section collapses back to nil on read, so a
membership-only group round-trips with no ingress or egress.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The backing CiliumNetworkPolicy carries the membership finalizer, so
deleting a SecurityGroup is virtually always asynchronous: the policy
gets a deletionTimestamp and survives until the securitygroup-controller
strips the membership labels off member pods and removes the finalizer.
REST.Delete nonetheless returned (nil, true), reporting the object as
instantly deleted, so the endpoint emitted a Success status claiming
completion instead of surfacing the terminating object — a
rest.GracefulDeleter contract violation for any client that distinguishes
the two to detect pending teardown.

After the delete, read the object's state back through the direct
(uncached) client and report deleted=true only when it is actually gone;
while a finalizer holds it, return deleted=false with the terminating
object. Using the direct client avoids a stale cache-backed read
misreporting an async delete as instant, and a dry-run (which leaves the
object in place) is resolved from the object's authoritative finalizers.
Implement rest.MayReturnFullObjectDeleter so the DELETE endpoint is
advertised and serialized as returning the SecurityGroup object, matching
the generic registry.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The ingress fromApp/fromSG projection is exercised end to end, but the
symmetric egress toApp/toSG path — toApp to a lineage-label
endpointSelector and toSG to the referenced group's membership-label
endpointSelector — had no test. A wrong label key there would silently
point an egress allow at the wrong endpoints. Pin the projection and the
reverse reconstruction with a Create+Get round-trip, mirroring the
existing ingress coverage.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-approve — the follow-up fixes are solid, several beyond cosmetic

Re-reviewed the new commits. All six land well, and a few are real correctness wins, not just polish:

  • Membership-only groups now create (cc4fb40da). A SecurityGroup with attachments but no ingress/egress — exactly the shape a group meant to be referenced by fromSG/toSG takes — projected to a selector-only spec that the cilium.io/v2 CRD rejects (spec anyOf requires a rule section). Emitting an always-present ingress: [] carrier fixes it, and the reasoning is the important part: an empty ingress list is inert in Cilium (it doesn't enable ingress default-deny), so the policy is schema-valid yet adds and denies nothing — consistent with the additive model, and still correct once the baseline becomes default-deny. Forcing a non-nil empty slice (so it can't marshal to null) and testing both the JSON and unstructured-converter paths is the right level of care.
  • Async delete reported correctly (a86817915). Because the backing policy carries the membership finalizer, deletion is virtually always asynchronous; returning the terminating object with deleted=false (read back through the direct client) and implementing MayReturnFullObjectDeleter matches the GracefulDeleter contract.
  • Two watch-proxy fixes (11ed2a960). Forwarding watch.Error (e.g. 410 Gone) so clients relist instead of seeing a clean close, and applying the field filter to every event type since name/namespace are immutable — both correct, both well-tested with a fake backing watch.
  • Empty-list doc semantics corrected (f03d2a510), OpenAPI regenerated to match. Reconciler error logging (881591d34) and build wiring (09f4d8bb8) round it out.

Every change ships with tests. The branch is in better shape than at the prior approval — the create-path and watch fixes close gaps that would have surfaced in production. Approving the current SHA.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit bf14a73 into main Jun 30, 2026
41 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/sdn-security-groups branch June 30, 2026 11:30
myasnikovdaniil added a commit that referenced this pull request Jul 8, 2026
Main added securitygroup.bats (#2922 membership model) and
serviceexposure.bats (#3081 ExposureClass/ServiceExposure) after the
Chainsaw migration branch was cut.

- securitygroup: two Tests — the aggregated-API projection round-trip
  (backing CNP marker label, empty-value membership endpointSelector,
  lineage fromApp + membership fromSG peers, 1:1 rule translation, the
  view hiding storage internals, unmarked-CNP invisibility via an error
  op, delete propagation) and the controller membership-label
  stamp/clear lifecycle. Only the attachments annotation stays a jq
  script (it is a JSON string). Chainsaw auto-cleanup covers every
  fixture even on mid-test failure — the bats had no teardown hook and
  leaked its fixtures.
- serviceexposure: the externalIPs backend resolve/Ready/assignedIPs
  path with the no-MetalLB-pool negative as an error op (ordered after
  resolvedBackend so it cannot false-pass), and the finalizer-removal
  test collapsed onto the delete op itself, which waits for full
  disappearance.
- selection: kubernetes-application now also maps to the two OIDC
  render-side suites (they exercise the same app chart — a gap main's
  bats selection had too); the new dirs enter all_apps discovery
  automatically. Selector unit tests extended and green (13/13 under
  cozytest).
- README: suites list updated.

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api Issues or PRs related to the cozystack-api aggregated API server area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants