Skip to content

Commit 3fec6ed

Browse files
authored
feat: force options on apply (#20)
## Description @lucasrod16 discovered a bug in Pepr when trying to `apply` a secret that was initially created by Zarf: ```typescript try { await K8s(kind.Secret).Apply({ metadata: { name: secretName, namespace: ns, }, data: { data: secret.data.data, }, }); } catch (err) { Log.error( `Error: Failed to update package secret '${secretName}' in namespace '${ns}'`, ); } ``` The error had to do with a `FieldManagerConflict`: ``` "Apply failed with 1 conflict: conflict with \\\"zarf\\\" using v1: .data.data\",\"reason\":\"Conflict\",\"details\":{\"causes\":[{\"reason\":\"FieldManagerConflict\",\"message\":\"conflict with \\\"zarf\\\" using v1\",\"field\":\".data.data\"}]},\"code\":409},\"ok\":false,\"status\":409,\"statusText\":\"Conflict\"}"} ``` There should be more options exposed in the future but this initial patch it to unblock @lucasrod16 . ## Related Issue Fixes #9 <!-- or --> Relates to # ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] 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 Steps](https://github.com/defenseunicorns/pepr/blob/main/CONTRIBUTING.md#submitting-a-pull-request) followed --------- Signed-off-by: Case Wylie <[email protected]>
1 parent 136503d commit 3fec6ed

File tree

6 files changed

+80
-6
lines changed

6 files changed

+80
-6
lines changed

src/fluent/apply.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3+
4+
/**
5+
* Configuration for the apply function.
6+
*/
7+
export type ApplyCfg = {
8+
/**
9+
* Force the apply to be a create.
10+
*/
11+
force?: boolean;
12+
};

src/fluent/index.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,50 @@ import { k8sExec } from "./utils";
88
// Setup mocks
99
jest.mock("./utils");
1010

11+
const generateFakePodManagedFields = (manager: string) => {
12+
return [
13+
{
14+
apiVersion: "v1",
15+
fieldsType: "FieldsV1",
16+
fieldsV1: {
17+
"f:metadata": {
18+
"f:labels": {
19+
"f:fake": {},
20+
},
21+
"f:spec": {
22+
"f:containers": {
23+
'k:{"name":"fake"}': {
24+
"f:image": {},
25+
"f:name": {},
26+
"f:resources": {
27+
"f:limits": {
28+
"f:cpu": {},
29+
"f:memory": {},
30+
},
31+
"f:requests": {
32+
"f:cpu": {},
33+
"f:memory": {},
34+
},
35+
},
36+
},
37+
},
38+
},
39+
},
40+
},
41+
manager: manager,
42+
operation: "Apply",
43+
},
44+
];
45+
};
1146
describe("Kube", () => {
12-
const fakeResource = { metadata: { name: "fake", namespace: "default" } };
47+
const fakeResource = {
48+
metadata: {
49+
name: "fake",
50+
namespace: "default",
51+
managedFields: generateFakePodManagedFields("pepr"),
52+
},
53+
};
54+
1355
const mockedKubeExec = jest.mocked(k8sExec).mockResolvedValue(fakeResource);
1456

1557
beforeEach(() => {
@@ -140,6 +182,18 @@ describe("Kube", () => {
140182
expect(result).toEqual(fakeResource);
141183
});
142184

185+
it("should allow force apply to resolve FieldManagerConflict", async () => {
186+
const kube = K8s(Pod);
187+
const result = await kube.Apply(
188+
{
189+
metadata: { name: "fake", managedFields: generateFakePodManagedFields("kubectl") },
190+
spec: { priority: 3 },
191+
},
192+
{ force: true },
193+
);
194+
expect(result).toEqual(fakeResource);
195+
});
196+
143197
it("should throw an error if a Delete failed for a reason other than Not Found", async () => {
144198
mockedKubeExec.mockRejectedValueOnce({ status: 500 }); // Internal Server Error on first call
145199
const kube = K8s(Pod);

src/fluent/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GenericClass } from "../types";
1111
import { Filters, K8sInit, Paths, WatchAction } from "./types";
1212
import { k8sExec } from "./utils";
1313
import { ExecWatch, WatchCfg } from "./watch";
14+
import { ApplyCfg } from "./apply";
1415

1516
/**
1617
* Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
@@ -100,9 +101,12 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
100101
}
101102
}
102103

103-
async function Apply(resource: PartialDeep<K>): Promise<K> {
104+
async function Apply(
105+
resource: PartialDeep<K>,
106+
applyCfg: ApplyCfg = { force: false },
107+
): Promise<K> {
104108
syncFilters(resource as K);
105-
return k8sExec(model, filters, "APPLY", resource);
109+
return k8sExec(model, filters, "APPLY", resource, applyCfg);
106110
}
107111

108112
async function Create(resource: K): Promise<K> {

src/fluent/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { PartialDeep } from "type-fest";
77

88
import { GenericClass, GroupVersionKind } from "../types";
99
import { WatchCfg, WatchController } from "./watch";
10+
import { ApplyCfg } from "./apply";
1011

1112
/**
1213
* The Phase matched when using the K8s Watch API.
@@ -65,7 +66,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
6566
* @param resource
6667
* @returns
6768
*/
68-
Apply: (resource: PartialDeep<K>) => Promise<K>;
69+
Apply: (resource: PartialDeep<K>, applyCfg?: ApplyCfg) => Promise<K>;
6970

7071
/**
7172
* Create the provided K8s resource or throw an error if it already exists.

src/fluent/utils.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ describe("pathBuilder Function", () => {
3333
"/api/v1/namespaces/default/pods/mypod?fieldSelector=iamafield%3Diamavalue&labelSelector=iamalabel%3Diamalabelvalue",
3434
serverUrl,
3535
);
36-
expect(result).toEqual(expected);
36+
37+
expect(result.toString()).toEqual(expected.toString());
3738
});
3839

3940
it("Version not specified in a Kind", () => {

src/fluent/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { fetch } from "../fetch";
99
import { modelToGroupVersionKind } from "../kinds";
1010
import { GenericClass } from "../types";
1111
import { FetchMethods, Filters } from "./types";
12+
import { ApplyCfg } from "./apply";
1213

1314
const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
1415

@@ -123,6 +124,7 @@ export async function k8sExec<T extends GenericClass, K>(
123124
filters: Filters,
124125
method: FetchMethods,
125126
payload?: K | unknown,
127+
applyCfg: ApplyCfg = { force: false },
126128
) {
127129
const { opts, serverUrl } = await k8sCfg(method);
128130
const url = pathBuilder(serverUrl, model, filters, method === "POST");
@@ -137,7 +139,7 @@ export async function k8sExec<T extends GenericClass, K>(
137139
opts.method = "PATCH";
138140
url.searchParams.set("fieldManager", "pepr");
139141
url.searchParams.set("fieldValidation", "Strict");
140-
url.searchParams.set("force", "false");
142+
url.searchParams.set("force", applyCfg.force ? "true" : "false");
141143
break;
142144
}
143145

0 commit comments

Comments
 (0)