Skip to content

Commit

Permalink
fix: kubeapi netpol initialization / support for ingress policies (#1097
Browse files Browse the repository at this point in the history
)

## Description

Fixes some issues with the netpol update logic to ensure we are
accounting for ingress policies, as well as ensuring this only runs on
watcher pods.

Also adds jest test coverage of this function.

## Related Issue

Fixes #1101

## Steps to Validate

The primary fix here has to do with Pepr crashing on startup when an
`Ingress` kubeapi policy is present. The below section steps through
testing this.

<details>
<summary>Validation Steps</summary>

```console
# Deploy base layer (using unicorn flavor to avoid dockerhub rate limiting)
uds run test-single-layer --set LAYER=base --set FLAVOR=unicorn
# Create a namespace for our test package
kubectl create ns test
# Create a package CR with egress and ingress kubeapi policies
cat <<EOF | kubectl apply -f -
apiVersion: uds.dev/v1alpha1
kind: Package
metadata:
  name: test
  namespace: test
spec:
  network:
    allow:
      - direction: Egress
        selector:
          app.kubernetes.io/name: test
        remoteGenerated: KubeAPI
      - direction: Ingress
        selector:
          app.kubernetes.io/name: test
        remoteGenerated: KubeAPI
EOF
# Validate netpols show up as expected
kubectl get networkpolicies -n test -o custom-columns="NAME:.metadata.name,INGRESS:.spec.ingress[].from[].ipBlock.cidr,EGRESS:.spec.egress[].to[].ipBlock.cidr" | grep kubeapi
# Cycle the pepr watcher pod (this is where pepr previously would crash, on startup when an ingress kubeapi policy was present)
kubectl delete po -n pepr-system -l app=pepr-uds-core-watcher
# Make sure pepr starts up as expected
# You could continue to stress test the logic by modifying the kubeapi netpols specs to be missing some fields, etc.
# Also worth reviewing the jest tests which should provide coverage of almost all situations at this point
```
</details>

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [x] [Contributor
Guide](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md)
followed

---------

Co-authored-by: Chance <[email protected]>
  • Loading branch information
mjnagel and UnicornChance authored Dec 10, 2024
1 parent f87a96d commit 620e6b2
Show file tree
Hide file tree
Showing 4 changed files with 372 additions and 19 deletions.
324 changes: 320 additions & 4 deletions src/pepr/operator/controllers/network/generators/kubeAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { K8s, kind } from "pepr";
import { updateAPIServerCIDR } from "./kubeAPI";
import { updateAPIServerCIDR, updateKubeAPINetworkPolicies } from "./kubeAPI";

type KubernetesList<T> = {
items: T[];
Expand All @@ -19,10 +19,10 @@ jest.mock("pepr", () => {
};
});

describe("updateAPIServerCIDR", () => {
const mockApply = jest.fn();
const mockGet = jest.fn<() => Promise<KubernetesList<kind.NetworkPolicy>>>();
const mockApply = jest.fn();
const mockGet = jest.fn<() => Promise<KubernetesList<kind.NetworkPolicy>>>();

describe("updateAPIServerCIDR", () => {
beforeEach(() => {
jest.clearAllMocks();
(K8s as jest.Mock).mockImplementation(() => ({
Expand Down Expand Up @@ -259,3 +259,319 @@ describe("updateAPIServerCIDR", () => {
expect(mockApply).not.toHaveBeenCalled();
});
});

describe("updateKubeAPINetworkPolicies", () => {
beforeEach(() => {
jest.clearAllMocks();
(K8s as jest.Mock).mockImplementation(() => ({
WithLabel: jest.fn(() => ({
Get: mockGet,
})),
Apply: mockApply,
}));
});

it("does not update an egress NetworkPolicy if the peers are already correct", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: newPeers,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).not.toHaveBeenCalled(); // No update needed
});

it("does not update an ingress NetworkPolicy if the peers are already correct", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: newPeers,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).not.toHaveBeenCalled(); // No update needed
});

it("updates an egress NetworkPolicy with different peers", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
const oldPeers = [{ ipBlock: { cidr: "192.168.1.0/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: oldPeers,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: newPeers,
},
],
},
}),
{ force: true },
);
});

it("updates an ingress NetworkPolicy with different peers", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
const oldPeers = [{ ipBlock: { cidr: "192.168.1.0/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: oldPeers,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: newPeers,
},
],
},
}),
{ force: true },
);
});

it("updates an egress NetworkPolicy with no peers", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: undefined,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: newPeers,
},
],
},
}),
{ force: true },
);
});

it("updates an ingress NetworkPolicy with no peers", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: undefined,
},
],
},
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: newPeers,
},
],
},
}),
{ force: true },
);
});

it("initializes missing egress rules", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [{}],
}, // No egress at all
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
egress: [
{
to: newPeers,
},
],
},
}),
{ force: true },
);
});

it("initializes missing ingress rules", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [
{
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [{}],
}, // No egress at all
},
],
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).toHaveBeenCalledWith(
expect.objectContaining({
metadata: {
name: "mock-netpol",
namespace: "default",
},
spec: {
ingress: [
{
from: newPeers,
},
],
},
}),
{ force: true },
);
});

it("handles no matching NetworkPolicies", async () => {
const newPeers = [{ ipBlock: { cidr: "10.0.0.1/32" } }];
mockGet.mockResolvedValue({
items: [], // No NetworkPolicies found
} as KubernetesList<kind.NetworkPolicy>);

await updateKubeAPINetworkPolicies(newPeers);

expect(mockGet).toHaveBeenCalled();
expect(mockApply).not.toHaveBeenCalled(); // No policies to update
});
});
Loading

0 comments on commit 620e6b2

Please sign in to comment.