Skip to content

Commit 9cc8d75

Browse files
committed
feat: added listing of podman images
1 parent 8cd7fc5 commit 9cc8d75

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2022 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
#
9+
# Get podman images
10+
#
11+
12+
import os
13+
import sys
14+
import json
15+
import subprocess
16+
import agent
17+
18+
# Prepare return variable
19+
images_data = {}
20+
21+
try:
22+
# Run podman images command to get images in JSON format
23+
result = subprocess.run(
24+
["podman", "images", "--format", "json"],
25+
capture_output=True,
26+
text=True,
27+
check=True,
28+
)
29+
images = json.loads(result.stdout)
30+
images_data["images"] = images
31+
images_data["error"] = None
32+
except subprocess.CalledProcessError as e:
33+
images_data["images"] = []
34+
images_data["error"] = f"Failed to get podman images: {e.stderr}"
35+
except json.JSONDecodeError as e:
36+
images_data["images"] = []
37+
images_data["error"] = f"Failed to parse podman images output: {str(e)}"
38+
except Exception as e:
39+
images_data["images"] = []
40+
images_data["error"] = f"Unexpected error: {str(e)}"
41+
42+
# Dump the images data to stdout
43+
json.dump(images_data, fp=sys.stdout)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Get podman images",
4+
"$id": "http://nethserver.org/json-schema/task/input/erpnext/podman-images",
5+
"description": "Get list of podman images",
6+
"examples": [
7+
{
8+
"images": [
9+
{
10+
"id": "sha256:abc123",
11+
"repositories": ["docker.io/library/nginx:latest"],
12+
"tags": ["latest"],
13+
"created": "2023-01-01 12:00:00 +0000 UTC",
14+
"size": "142MB"
15+
}
16+
],
17+
"error": null
18+
}
19+
],
20+
"type": "object",
21+
"required": ["images", "error"],
22+
"properties": {
23+
"images": {
24+
"type": "array",
25+
"title": "Podman images list",
26+
"description": "List of podman images with their details",
27+
"items": {
28+
"type": "object",
29+
"properties": {
30+
"id": {
31+
"type": "string",
32+
"description": "Image ID"
33+
},
34+
"repositories": {
35+
"type": "array",
36+
"items": {
37+
"type": "string"
38+
},
39+
"description": "Image repositories"
40+
},
41+
"tags": {
42+
"type": "array",
43+
"items": {
44+
"type": "string"
45+
},
46+
"description": "Image tags"
47+
},
48+
"created": {
49+
"type": "string",
50+
"description": "Creation timestamp"
51+
},
52+
"size": {
53+
"type": "string",
54+
"description": "Image size"
55+
}
56+
}
57+
}
58+
},
59+
"error": {
60+
"type": ["string", "null"],
61+
"title": "Error message",
62+
"description": "Error message if the operation failed, null otherwise"
63+
}
64+
}
65+
}

ui/src/views/Settings.vue

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,92 @@
9494
<h4>Manage Apps</h4>
9595
</cv-column>
9696
</cv-row>
97+
98+
<cv-row class="mg-bottom">
99+
<cv-column :sm="4">
100+
<h4>Podman Images</h4>
101+
</cv-column>
102+
</cv-row>
103+
<cv-row class="mg-bottom">
104+
<cv-column>
105+
<cv-tile light>
106+
<div v-if="error.getPodmanImages" class="error-section">
107+
<NsInlineNotification
108+
kind="error"
109+
:title="$t('action.get-podman-images')"
110+
:description="error.getPodmanImages"
111+
:showCloseButton="false"
112+
/>
113+
</div>
114+
<div class="images-container">
115+
<div v-if="loading.getPodmanImages" class="loading-section">
116+
<cv-loading>Loading images...</cv-loading>
117+
</div>
118+
<div
119+
v-else-if="podmanImages.length === 0"
120+
class="empty-state"
121+
>
122+
<p>No podman images found</p>
123+
</div>
124+
<cv-structured-list v-else>
125+
<template slot="headings">
126+
<cv-structured-list-heading
127+
>Repository</cv-structured-list-heading
128+
>
129+
<cv-structured-list-heading
130+
>Tag</cv-structured-list-heading
131+
>
132+
<cv-structured-list-heading
133+
>Image ID</cv-structured-list-heading
134+
>
135+
<cv-structured-list-heading
136+
>Created</cv-structured-list-heading
137+
>
138+
<cv-structured-list-heading
139+
>Size</cv-structured-list-heading
140+
>
141+
</template>
142+
<template slot="items">
143+
<cv-structured-list-item
144+
v-for="(image, index) in podmanImages"
145+
:key="index"
146+
>
147+
<cv-structured-list-data>{{
148+
image.repositories && image.repositories.length > 0
149+
? image.repositories[0]
150+
: "N/A"
151+
}}</cv-structured-list-data>
152+
<cv-structured-list-data>{{
153+
image.tags && image.tags.length > 0
154+
? image.tags[0]
155+
: "N/A"
156+
}}</cv-structured-list-data>
157+
<cv-structured-list-data>{{
158+
image.id ? image.id.substring(0, 12) : "N/A"
159+
}}</cv-structured-list-data>
160+
<cv-structured-list-data>{{
161+
image.created || "N/A"
162+
}}</cv-structured-list-data>
163+
<cv-structured-list-data>{{
164+
image.size || "N/A"
165+
}}</cv-structured-list-data>
166+
</cv-structured-list-item>
167+
</template>
168+
</cv-structured-list>
169+
</div>
170+
<div class="images-actions">
171+
<cv-button
172+
kind="tertiary"
173+
:icon="Refresh20"
174+
@click.prevent="getPodmanImages"
175+
:disabled="loading.getPodmanImages"
176+
>
177+
Refresh Images
178+
</cv-button>
179+
</div>
180+
</cv-tile>
181+
</cv-column>
182+
</cv-row>
97183
<cv-row class="mg-bottom">
98184
<cv-column :sm="12">
99185
<cv-tile light>
@@ -267,6 +353,7 @@
267353
<script>
268354
import to from "await-to-js";
269355
import { mapState } from "vuex";
356+
import { Refresh20, TrashCan20, Save20, Add20 } from "@carbon/icons-vue";
270357
import {
271358
QueryParamService,
272359
UtilService,
@@ -277,6 +364,12 @@ import {
277364
278365
export default {
279366
name: "Settings",
367+
components: {
368+
Refresh20,
369+
TrashCan20,
370+
Save20,
371+
Add20,
372+
},
280373
281374
mixins: [
282375
TaskService,
@@ -300,6 +393,7 @@ export default {
300393
hasBackup: false,
301394
302395
erpSelectedModules: [],
396+
podmanImages: [],
303397
app_json: "",
304398
appJsonError: "",
305399
frappeVersion: "version-15",
@@ -319,13 +413,15 @@ export default {
319413
getConfiguration: false,
320414
configureModule: false,
321415
buildDockerImage: false,
416+
getPodmanImages: false,
322417
},
323418
error: {
324419
getConfiguration: "",
325420
configureModule: "",
326421
host: "",
327422
lets_encrypt: "",
328423
http2https: "",
424+
getPodmanImages: "",
329425
},
330426
};
331427
},
@@ -376,6 +472,7 @@ export default {
376472
},
377473
created() {
378474
this.getConfiguration();
475+
this.getPodmanImages();
379476
},
380477
beforeRouteEnter(to, from, next) {
381478
next((vm) => {
@@ -388,6 +485,56 @@ export default {
388485
next();
389486
},
390487
methods: {
488+
async getPodmanImages() {
489+
this.loading.getPodmanImages = true;
490+
this.error.getPodmanImages = "";
491+
const taskAction = "podman-images-module";
492+
const eventId = this.getUuid();
493+
494+
// register to task error
495+
this.core.$root.$once(
496+
`${taskAction}-aborted-${eventId}`,
497+
this.getPodmanImagesAborted
498+
);
499+
500+
// register to task completion
501+
this.core.$root.$once(
502+
`${taskAction}-completed-${eventId}`,
503+
this.getPodmanImagesCompleted
504+
);
505+
506+
const res = await to(
507+
this.createModuleTaskForApp(this.instanceName, {
508+
action: taskAction,
509+
extra: {
510+
title: this.$t("action." + taskAction),
511+
isNotificationHidden: true,
512+
eventId,
513+
},
514+
})
515+
);
516+
const err = res[0];
517+
518+
if (err) {
519+
console.error(`error creating task ${taskAction}`, err);
520+
this.error.getPodmanImages = this.getErrorMessage(err);
521+
this.loading.getPodmanImages = false;
522+
return;
523+
}
524+
},
525+
getPodmanImagesAborted(taskResult, taskContext) {
526+
console.error(`${taskContext.action} aborted`, taskResult);
527+
this.error.getPodmanImages = this.$t("error.generic_error");
528+
this.loading.getPodmanImages = false;
529+
},
530+
getPodmanImagesCompleted(taskContext, taskResult) {
531+
const imagesData = taskResult.output;
532+
this.podmanImages = imagesData.images || [];
533+
if (imagesData.error) {
534+
this.error.getPodmanImages = imagesData.error;
535+
}
536+
this.loading.getPodmanImages = false;
537+
},
391538
openAddAppModal() {
392539
this.newApp = {
393540
app_name: "",
@@ -822,4 +969,27 @@ export default {
822969
align-items: center;
823970
gap: 0.5rem;
824971
}
972+
973+
.images-container {
974+
max-height: 400px;
975+
overflow-y: auto;
976+
margin-bottom: $spacing-06;
977+
}
978+
979+
.images-actions {
980+
display: flex;
981+
gap: $spacing-04;
982+
align-items: center;
983+
}
984+
985+
.error-section {
986+
margin-bottom: $spacing-06;
987+
}
988+
989+
.loading-section {
990+
display: flex;
991+
justify-content: center;
992+
align-items: center;
993+
padding: $spacing-06;
994+
}
825995
</style>

0 commit comments

Comments
 (0)