fix: preserve bound PVC storage classes
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ import { trust } from "./trust";
|
|||||||
export const main = defineCommand({
|
export const main = defineCommand({
|
||||||
meta: {
|
meta: {
|
||||||
name: "kuber",
|
name: "kuber",
|
||||||
version: "2.3.1",
|
version: "2.3.2",
|
||||||
description: "Docker Compose -> K8s translation layer",
|
description: "Docker Compose -> K8s translation layer",
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dmgnr/kuber",
|
"name": "@dmgnr/kuber",
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"description": "Docker Compose to Kubernetes translation layer",
|
"description": "Docker Compose to Kubernetes translation layer",
|
||||||
"bin": {
|
"bin": {
|
||||||
"kuber": "dist/index.js"
|
"kuber": "dist/index.js"
|
||||||
|
|||||||
@@ -984,20 +984,35 @@ export class KubernetesTrustStore implements TrustStore {
|
|||||||
}
|
}
|
||||||
async list(project: string): Promise<string[]> {
|
async list(project: string): Promise<string[]> {
|
||||||
return (await list(this.objects, "ConfigMap", "trust", project))
|
return (await list(this.objects, "ConfigMap", "trust", project))
|
||||||
.map((item) => parsePayload<{ project: string; fingerprint: string }>(item))
|
.map((item) =>
|
||||||
|
parsePayload<{ project: string; fingerprint: string }>(item),
|
||||||
|
)
|
||||||
.filter((record): record is { project: string; fingerprint: string } =>
|
.filter((record): record is { project: string; fingerprint: string } =>
|
||||||
Boolean(record && record.project === project && /^[a-f0-9]{64}$/.test(record.fingerprint)),
|
Boolean(
|
||||||
|
record &&
|
||||||
|
record.project === project &&
|
||||||
|
/^[a-f0-9]{64}$/.test(record.fingerprint),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map((record) => record.fingerprint);
|
.map((record) => record.fingerprint);
|
||||||
}
|
}
|
||||||
async has(project: string, fingerprint: string): Promise<boolean> {
|
async has(project: string, fingerprint: string): Promise<boolean> {
|
||||||
const item = await read(this.objects, "ConfigMap", this.name(project, fingerprint));
|
const item = await read(
|
||||||
const record = item && parsePayload<{ project: string; fingerprint: string }>(item);
|
this.objects,
|
||||||
|
"ConfigMap",
|
||||||
|
this.name(project, fingerprint),
|
||||||
|
);
|
||||||
|
const record =
|
||||||
|
item && parsePayload<{ project: string; fingerprint: string }>(item);
|
||||||
return record?.project === project && record.fingerprint === fingerprint;
|
return record?.project === project && record.fingerprint === fingerprint;
|
||||||
}
|
}
|
||||||
async revoke(project: string, fingerprint: string): Promise<boolean> {
|
async revoke(project: string, fingerprint: string): Promise<boolean> {
|
||||||
if (!(await this.has(project, fingerprint))) return false;
|
if (!(await this.has(project, fingerprint))) return false;
|
||||||
return deleteObject(this.objects, "ConfigMap", this.name(project, fingerprint));
|
return deleteObject(
|
||||||
|
this.objects,
|
||||||
|
"ConfigMap",
|
||||||
|
this.name(project, fingerprint),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,6 +1086,53 @@ export function createKubernetesManagementDependencies(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
const overrides: Partial<ManagementDependencies> = {
|
const overrides: Partial<ManagementDependencies> = {
|
||||||
|
applyResource: async (desired) => {
|
||||||
|
let resource = desired as KubernetesObject & {
|
||||||
|
spec?: { storageClassName?: string };
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
desired.apiVersion === "v1" &&
|
||||||
|
desired.kind === "PersistentVolumeClaim" &&
|
||||||
|
desired.metadata?.name
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const existing = (await objects.read({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "PersistentVolumeClaim",
|
||||||
|
metadata: {
|
||||||
|
name: desired.metadata.name,
|
||||||
|
namespace: desired.metadata.namespace,
|
||||||
|
},
|
||||||
|
})) as KubernetesObject & {
|
||||||
|
spec?: { storageClassName?: string };
|
||||||
|
status?: { phase?: string };
|
||||||
|
};
|
||||||
|
const spec = resource.spec;
|
||||||
|
if (
|
||||||
|
existing.status?.phase === "Bound" &&
|
||||||
|
existing.spec?.storageClassName !== spec?.storageClassName
|
||||||
|
) {
|
||||||
|
resource = {
|
||||||
|
...desired,
|
||||||
|
spec: {
|
||||||
|
...spec,
|
||||||
|
storageClassName: existing.spec?.storageClassName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!isNotFound(error)) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects.patch(
|
||||||
|
resource,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
FIELD_MANAGER,
|
||||||
|
true,
|
||||||
|
PatchStrategy.ServerSideApply,
|
||||||
|
);
|
||||||
|
},
|
||||||
listDeployments: async (project) =>
|
listDeployments: async (project) =>
|
||||||
(
|
(
|
||||||
await apps.listNamespacedDeployment({
|
await apps.listNamespacedDeployment({
|
||||||
|
|||||||
@@ -836,12 +836,12 @@ type ManagementClientsType = NonNullable<
|
|||||||
const managementClients = (
|
const managementClients = (
|
||||||
deployments: Array<Record<string, unknown>>,
|
deployments: Array<Record<string, unknown>>,
|
||||||
replicaSets: Array<Record<string, unknown>>,
|
replicaSets: Array<Record<string, unknown>>,
|
||||||
|
objects = {} as KubernetesObjectApi,
|
||||||
): ManagementClientsType => {
|
): ManagementClientsType => {
|
||||||
const apps = new FakeApps(deployments, replicaSets);
|
const apps = new FakeApps(deployments, replicaSets);
|
||||||
const objects = {} as KubernetesObjectApi;
|
|
||||||
return {
|
return {
|
||||||
apps: apps as unknown as ManagementClientsType["apps"],
|
apps: apps as unknown as ManagementClientsType["apps"],
|
||||||
objects: {} as KubernetesObjectApi,
|
objects,
|
||||||
config: {} as ManagementClientsType["config"],
|
config: {} as ManagementClientsType["config"],
|
||||||
batch: {} as ManagementClientsType["batch"],
|
batch: {} as ManagementClientsType["batch"],
|
||||||
core: {} as ManagementClientsType["core"],
|
core: {} as ManagementClientsType["core"],
|
||||||
@@ -849,6 +849,107 @@ const managementClients = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PersistentVolumeClaim = KubernetesObject & {
|
||||||
|
spec: { storageClassName?: string };
|
||||||
|
status?: { phase?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
function persistentVolumeClaim(
|
||||||
|
storageClassName = "kuber-longhorn-test",
|
||||||
|
): PersistentVolumeClaim {
|
||||||
|
return {
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "PersistentVolumeClaim",
|
||||||
|
metadata: { name: "kimaki-kimaki-data", namespace: "kimaki" },
|
||||||
|
spec: { storageClassName },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function managementObjects(existing?: KubernetesObject) {
|
||||||
|
const applied: PersistentVolumeClaim[] = [];
|
||||||
|
return {
|
||||||
|
applied,
|
||||||
|
objects: {
|
||||||
|
async read() {
|
||||||
|
if (!existing) throw { code: 404 };
|
||||||
|
return structuredClone(existing);
|
||||||
|
},
|
||||||
|
async patch(resource: KubernetesObject) {
|
||||||
|
applied.push(structuredClone(resource) as PersistentVolumeClaim);
|
||||||
|
return resource;
|
||||||
|
},
|
||||||
|
} as unknown as KubernetesObjectApi,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("management PVC storage classes", () => {
|
||||||
|
test("sends the existing storage class for a Bound claim with a different class", async () => {
|
||||||
|
const existing = {
|
||||||
|
...persistentVolumeClaim("longhorn"),
|
||||||
|
status: { phase: "Bound" },
|
||||||
|
};
|
||||||
|
const fake = managementObjects(existing);
|
||||||
|
const dependencies = createKubernetesManagementDependencies(
|
||||||
|
managementClients([], [], fake.objects),
|
||||||
|
);
|
||||||
|
const desired = persistentVolumeClaim();
|
||||||
|
|
||||||
|
await dependencies.applyResource(desired);
|
||||||
|
|
||||||
|
expect(fake.applied).toEqual([
|
||||||
|
{
|
||||||
|
...persistentVolumeClaim(),
|
||||||
|
spec: { storageClassName: "longhorn" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(desired).toEqual(persistentVolumeClaim());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("retains a matching storage class for a Bound claim", async () => {
|
||||||
|
const existing = {
|
||||||
|
...persistentVolumeClaim(),
|
||||||
|
status: { phase: "Bound" },
|
||||||
|
};
|
||||||
|
const fake = managementObjects(existing);
|
||||||
|
const dependencies = createKubernetesManagementDependencies(
|
||||||
|
managementClients([], [], fake.objects),
|
||||||
|
);
|
||||||
|
const desired = persistentVolumeClaim();
|
||||||
|
|
||||||
|
await dependencies.applyResource(desired);
|
||||||
|
|
||||||
|
expect(fake.applied).toEqual([desired]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("retains the desired storage class for an absent claim", async () => {
|
||||||
|
const fake = managementObjects();
|
||||||
|
const dependencies = createKubernetesManagementDependencies(
|
||||||
|
managementClients([], [], fake.objects),
|
||||||
|
);
|
||||||
|
const desired = persistentVolumeClaim();
|
||||||
|
|
||||||
|
await dependencies.applyResource(desired);
|
||||||
|
|
||||||
|
expect(fake.applied).toEqual([desired]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("retains a different storage class for a Pending claim", async () => {
|
||||||
|
const existing = {
|
||||||
|
...persistentVolumeClaim("longhorn"),
|
||||||
|
status: { phase: "Pending" },
|
||||||
|
};
|
||||||
|
const fake = managementObjects(existing);
|
||||||
|
const dependencies = createKubernetesManagementDependencies(
|
||||||
|
managementClients([], [], fake.objects),
|
||||||
|
);
|
||||||
|
const desired = persistentVolumeClaim();
|
||||||
|
|
||||||
|
await dependencies.applyResource(desired);
|
||||||
|
|
||||||
|
expect(fake.applied).toEqual([desired]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("rollback ReplicaSet discovery", () => {
|
describe("rollback ReplicaSet discovery", () => {
|
||||||
test("discovers owned revisions without the managed-by selector", async () => {
|
test("discovers owned revisions without the managed-by selector", async () => {
|
||||||
const depUid = "deployment-uid";
|
const depUid = "deployment-uid";
|
||||||
|
|||||||
Reference in New Issue
Block a user