Skip to content

Commit

Permalink
chore: extract deployment check functions to new file for ease of mai…
Browse files Browse the repository at this point in the history
…ntenance (#1472)

## Description

`helpers.ts` is bloated and adds to maintenance overhead. By moving some
deployment-related functions to a separate file we reduce the cognitive
load faced by any developer working withing `helpers.ts` and associated
tests.

End to End Test:  <!-- if applicable -->  
(See [Pepr Excellent
Examples](https://github.com/defenseunicorns/pepr-excellent-examples))

## Related Issue

Relates to #1397

## Type of change

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

## Checklist before merging
- [x] Unit,
[Journey](https://github.com/defenseunicorns/pepr/tree/main/journey),
[E2E Tests](https://github.com/defenseunicorns/pepr-excellent-examples),
[docs](https://github.com/defenseunicorns/pepr/tree/main/docs),
[adr](https://github.com/defenseunicorns/pepr/tree/main/adr) added or
updated as needed
- [x] [Contributor Guide
Steps](https://docs.pepr.dev/main/contribute/#submitting-a-pull-request)
followed
  • Loading branch information
samayer12 authored Nov 22, 2024
1 parent 74a1998 commit d2dec12
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 278 deletions.
3 changes: 2 additions & 1 deletion src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import prompt from "prompts";
import { Assets } from "../lib/assets";
import { buildModule } from "./build";
import { RootCmd } from "./root";
import { validateCapabilityNames, namespaceDeploymentsReady } from "../lib/helpers";
import { validateCapabilityNames } from "../lib/helpers";
import { ImagePullSecret } from "../lib/types";
import { sanitizeName } from "./init/utils";
import { deployImagePullSecret } from "../lib/assets/deploy";
import { namespaceDeploymentsReady } from "../lib/deploymentChecks";

export default function (program: RootCmd) {
program
Expand Down
243 changes: 243 additions & 0 deletions src/lib/deploymentChecks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { describe, jest, test, beforeEach, afterEach, expect } from "@jest/globals";
import { K8s, GenericClass, KubernetesObject } from "kubernetes-fluent-client";
import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types";
import { checkDeploymentStatus, namespaceDeploymentsReady } from "./deploymentChecks";

jest.mock("kubernetes-fluent-client", () => {
return {
K8s: jest.fn(),
kind: jest.fn(),
};
});

describe("namespaceDeploymentsReady", () => {
const mockK8s = jest.mocked(K8s);

beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
jest.useRealTimers();
});

test("should return true if all deployments are ready", async () => {
const deployments = {
items: [
{
metadata: {
name: "watcher",
namespace: "pepr-system",
},
spec: {
replicas: 1,
},
status: {
readyReplicas: 1,
},
},
{
metadata: {
name: "admission",
namespace: "pepr-system",
},
spec: {
replicas: 2,
},
status: {
readyReplicas: 2,
},
},
],
};

mockK8s.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
return {
InNamespace: jest.fn().mockReturnThis(),
Get: () => deployments,
} as unknown as K8sInit<T, K>;
});

const expected = true;
const result = await namespaceDeploymentsReady();
expect(result).toBe(expected);
});

test("should call checkDeploymentStatus if any deployments are not ready", async () => {
const deployments = {
items: [
{
metadata: {
name: "watcher",
namespace: "pepr-system",
},
spec: {
replicas: 1,
},
status: {
readyReplicas: 1,
},
},
{
metadata: {
name: "admission",
namespace: "pepr-system",
},
spec: {
replicas: 2,
},
status: {
readyReplicas: 1,
},
},
],
};

const deployments2 = {
items: [
{
metadata: {
name: "watcher",
namespace: "pepr-system",
},
spec: {
replicas: 1,
},
status: {
readyReplicas: 1,
},
},
{
metadata: {
name: "admission",
namespace: "pepr-system",
},
spec: {
replicas: 2,
},
status: {
readyReplicas: 2,
},
},
],
};

mockK8s
.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
return {
InNamespace: jest.fn().mockReturnThis(),
Get: () => deployments,
} as unknown as K8sInit<T, K>;
})
.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
return {
InNamespace: jest.fn().mockReturnThis(),
Get: () => deployments2,
} as unknown as K8sInit<T, K>;
});

const expected = true;
const result = await namespaceDeploymentsReady();

expect(result).toBe(expected);

expect(mockK8s).toHaveBeenCalledTimes(1);
});
});

describe("checkDeploymentStatus", () => {
const mockK8s = jest.mocked(K8s);

beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
jest.useRealTimers();
});
test("should return true if all deployments are ready", async () => {
const deployments = {
items: [
{
metadata: {
name: "watcher",
namespace: "pepr-system",
},
spec: {
replicas: 1,
},
status: {
readyReplicas: 1,
},
},
{
metadata: {
name: "admission",
namespace: "pepr-system",
},
spec: {
replicas: 2,
},
status: {
readyReplicas: 2,
},
},
],
};

mockK8s.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
return {
InNamespace: jest.fn().mockReturnThis(),
Get: () => deployments,
} as unknown as K8sInit<T, K>;
});

const expected = true;
const result = await checkDeploymentStatus("pepr-system");
expect(result).toBe(expected);
});

test("should return false if any deployments are not ready", async () => {
const deployments = {
items: [
{
metadata: {
name: "watcher",
namespace: "pepr-system",
},
spec: {
replicas: 1,
},
status: {
readyReplicas: 1,
},
},
{
metadata: {
name: "admission",
namespace: "pepr-system",
},
spec: {
replicas: 2,
},
status: {
readyReplicas: 1,
},
},
],
};

mockK8s.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
return {
InNamespace: jest.fn().mockReturnThis(),
Get: () => deployments,
} as unknown as K8sInit<T, K>;
});

const expected = false;
const result = await checkDeploymentStatus("pepr-system");
expect(result).toBe(expected);
});
});
43 changes: 43 additions & 0 deletions src/lib/deploymentChecks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// check to see if all replicas are ready for all deployments in the pepr-system namespace

import { K8s, kind } from "kubernetes-fluent-client";
import Log from "./logger";

// returns true if all deployments are ready, false otherwise
export async function checkDeploymentStatus(namespace: string) {
const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get();
let status = false;
let readyCount = 0;

for (const deployment of deployments.items) {
const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0;
if (deployment.status?.readyReplicas !== deployment.spec?.replicas) {
Log.info(
`Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
);
} else {
Log.info(
`Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
);
readyCount++;
}
}
if (readyCount === deployments.items.length) {
status = true;
}
return status;
}

// wait for all deployments in the pepr-system namespace to be ready
export async function namespaceDeploymentsReady(namespace: string = "pepr-system") {
Log.info(`Checking ${namespace} deployments status...`);
let ready = false;
while (!ready) {
ready = await checkDeploymentStatus(namespace);
if (ready) {
return ready;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
Log.info(`All ${namespace} deployments are ready`);
}
Loading

0 comments on commit d2dec12

Please sign in to comment.