Skip to content

Commit ef27acb

Browse files
authored
Merge pull request #21 from bryopsida/20-filter-resources-considered-for-updates
Filter to only target resources with pullPolicy=Always
2 parents 8e9093f + cb95d27 commit ef27acb

File tree

7 files changed

+173
-11
lines changed

7 files changed

+173
-11
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ This is still very early in development and has a few limitations.
1010

1111
1. Does not support private registries
1212
2. Does not pull updates for things without `imagePullPolicy = Always`
13-
3. Only supports querying `linux/arm64` image repositories at the moment
14-
4. Only looks for updates to the same tag, IE for cases where base patches have been pushed to a tag
13+
3. Only looks for updates to the same tag, IE for cases where base patches have been pushed to a tag
1514

1615
## How to deploy
1716

charts/patchwork/Chart.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ apiVersion: v2
22
name: patchwork
33
description: Watches deployments, daemonsets, and statefulsets for image updates and will automatically trigger rollouts to pull in updates
44
type: application
5-
version: 0.5.0
6-
appVersion: '0.3.0'
5+
version: 0.6.0
6+
appVersion: '0.4.0'
77
dependencies:
88
- name: redis
99
repository: https://groundhog2k.github.io/helm-charts/

charts/patchwork/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# patchwork
22

3-
![Version: 0.5.0](https://img.shields.io/badge/Version-0.5.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.3.0](https://img.shields.io/badge/AppVersion-0.3.0-informational?style=flat-square)
3+
![Version: 0.6.0](https://img.shields.io/badge/Version-0.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.4.0](https://img.shields.io/badge/AppVersion-0.4.0-informational?style=flat-square)
44

55
Watches deployments, daemonsets, and statefulsets for image updates and will automatically trigger rollouts to pull in updates
66

package-lock.json

+13-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "patchwork",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "",
55
"author": "",
66
"private": true,
@@ -73,6 +73,7 @@
7373
"testcontainers": "^9.9.1",
7474
"ts-jest": "29.1.0",
7575
"ts-loader": "^9.2.3",
76+
"ts-mockito": "^2.6.1",
7677
"ts-node": "^10.0.0",
7778
"tsconfig-paths": "4.2.0",
7879
"typescript": "^5.0.0"

src/kubernetes/k8s.service.spec.ts

+147-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { Test, TestingModule } from '@nestjs/testing'
22
import { K8sService } from './k8s.service'
3+
import { AppsV1Api, CoreV1Api } from '@kubernetes/client-node'
4+
import { mock, when, instance, anything } from 'ts-mockito'
35

46
describe('K8sService', () => {
57
let service: K8sService
8+
let mockCore: CoreV1Api
9+
let mockAppApi: AppsV1Api
610

711
beforeEach(async () => {
812
const module: TestingModule = await Test.createTestingModule({
@@ -11,7 +15,16 @@ describe('K8sService', () => {
1115
{
1216
provide: 'K8S_CONFIG',
1317
useValue: {
14-
makeApiClient: jest.fn(),
18+
makeApiClient: (type: any) => {
19+
switch (type.name.toLowerCase()) {
20+
case 'corev1api':
21+
mockCore = mock<CoreV1Api>()
22+
return instance(mockCore)
23+
default:
24+
mockAppApi = mock<AppsV1Api>()
25+
return instance(mockAppApi)
26+
}
27+
},
1528
},
1629
},
1730
],
@@ -23,4 +36,137 @@ describe('K8sService', () => {
2336
it('should be defined', () => {
2437
expect(service).toBeDefined()
2538
})
39+
40+
it('should not return images without pullPolicy: always', async () => {
41+
// GIVEN
42+
/*
43+
const allDeploymentsProm = this.appClient.listDeploymentForAllNamespaces()
44+
const allDaemonsetsProm = this.appClient.listDaemonSetForAllNamespaces()
45+
const allStatefulsetsProm = this.appClient.listStatefulSetForAllNamespaces()
46+
*/
47+
when(mockAppApi.listDeploymentForAllNamespaces()).thenResolve({
48+
response: {} as unknown as any,
49+
body: {
50+
items: [
51+
{
52+
metadata: {
53+
namesace: 'TEST',
54+
},
55+
spec: {
56+
selector: {
57+
matchLabels: {
58+
test: 'TEST-DEPLOYMENT',
59+
},
60+
},
61+
},
62+
},
63+
],
64+
} as unknown as any,
65+
})
66+
when(mockAppApi.listStatefulSetForAllNamespaces()).thenResolve({
67+
response: {} as unknown as any,
68+
body: {
69+
items: [
70+
{
71+
metadata: {
72+
namespace: 'TEST',
73+
},
74+
spec: {
75+
selector: {
76+
matchLabels: {
77+
test: 'TEST-DEPLOYMENT',
78+
},
79+
},
80+
},
81+
},
82+
],
83+
} as unknown as any,
84+
})
85+
when(mockAppApi.listDaemonSetForAllNamespaces()).thenResolve({
86+
response: {} as unknown as any,
87+
body: {
88+
items: [
89+
{
90+
metadata: {
91+
namespace: 'TEST',
92+
},
93+
spec: {
94+
selector: {
95+
matchLabels: {
96+
test: 'TEST-DEPLOYMENT',
97+
},
98+
},
99+
},
100+
},
101+
],
102+
} as unknown as any,
103+
})
104+
when(
105+
mockCore.listNamespacedPod(
106+
anything(),
107+
anything(),
108+
anything(),
109+
anything(),
110+
anything(),
111+
anything()
112+
)
113+
).thenResolve({
114+
response: {} as unknown as any,
115+
body: {
116+
items: [
117+
{
118+
status: {
119+
containerStatuses: [
120+
{
121+
name: 'test1',
122+
started: true,
123+
imageID: 'name@TEST',
124+
},
125+
{
126+
name: 'test2',
127+
started: true,
128+
imageID: 'name@TEST',
129+
},
130+
],
131+
},
132+
spec: {
133+
nodeName: 'TEST-NODE',
134+
containers: [
135+
{
136+
name: 'test1',
137+
image: 'busybox:latest',
138+
imagePullPolicy: 'Always',
139+
},
140+
{
141+
name: 'test2',
142+
image: 'alpine:latest',
143+
imagePullPolicy: 'Never',
144+
},
145+
],
146+
},
147+
},
148+
] as unknown as any,
149+
},
150+
})
151+
when(mockCore.readNode(anything())).thenResolve({
152+
response: {} as unknown as any,
153+
body: {
154+
metadata: {
155+
name: 'TEST-NODE',
156+
labels: {},
157+
},
158+
} as unknown as any,
159+
})
160+
161+
// WHEN
162+
const imageList = await service.getImageList()
163+
164+
// THEN
165+
expect(
166+
imageList.some(
167+
(iD) => iD.pullPolicy === 'IfNotPresent' || iD.pullPolicy === 'Never'
168+
)
169+
).toBeFalsy()
170+
expect(imageList.length).toEqual(3)
171+
})
26172
})

src/kubernetes/k8s.service.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ImageDescriptor {
3535
owner: Resource
3636
nodes: string[]
3737
arch: string
38+
pullPolicy: string
3839
}
3940

4041
export interface IK8sService {
@@ -95,6 +96,10 @@ function getImageHash(container: V1Container, pod: V1Pod): string {
9596
return matchedStatuses[0].imageID.split('@')[1]
9697
}
9798

99+
function getPullPolicy(container: V1Container): string {
100+
return container.imagePullPolicy
101+
}
102+
98103
function getResourceType(
99104
controllerObj: V1StatefulSet | V1Deployment | V1DaemonSet
100105
): ResourceType {
@@ -126,6 +131,7 @@ async function getImageDescriptors(
126131
const node = await getNode(pod.spec.nodeName, coreClient)
127132
return pod.spec.containers.map((container: V1Container): ImageDescriptor => {
128133
return {
134+
pullPolicy: getPullPolicy(container),
129135
repository: getImageRepo(container),
130136
tag: getImageTag(container),
131137
hash: getImageHash(container, pod),
@@ -183,7 +189,7 @@ export class K8sService implements IK8sService {
183189
)
184190
)
185191
.flat()
186-
.filter((obj) => obj != null)
192+
.filter((obj) => obj != null && obj.pullPolicy === 'Always')
187193
}
188194

189195
async triggerRollingUpdate(resource: Resource): Promise<void> {

0 commit comments

Comments
 (0)