diff --git a/cmd/root.go b/cmd/root.go index 094a133..746f6d4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -190,7 +190,10 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, filter)) }, updateLock) + updateHandler := update.New( + func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, filter)) }, + func(containers []string) { runUpdatesWithNotifications(filters.FilterByNames(containers, filter)) }, + updateLock) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) // If polling isn't enabled the scheduler is never started and // we need to trigger the startup messages manually. diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index ba044ab..9c1ce85 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -14,7 +14,7 @@ var ( ) // New is a factory function creating a new Handler instance -func New(updateFn func(images []string), updateLock chan bool) *Handler { +func New(updateImages func(images []string), updateContainers func(containerNames []string), updateLock chan bool) *Handler { if updateLock != nil { lock = updateLock } else { @@ -23,15 +23,17 @@ func New(updateFn func(images []string), updateLock chan bool) *Handler { } return &Handler{ - fn: updateFn, - Path: "/v1/update", + updateImages: updateImages, + updateContainers: updateContainers, + Path: "/v1/update", } } // Handler is an API handler used for triggering container update scans type Handler struct { - fn func(images []string) - Path string + updateImages func(images []string) + updateContainers func(containerNames []string) + Path string } // Handle is the actual http.Handle function doing all the heavy lifting @@ -55,15 +57,30 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { images = nil } + var containers []string + containerQueries, found := r.URL.Query()["container"] + if found { + for _, container := range containerQueries { + containers = append(containers, strings.Split(container, ",")...) + } + + } else { + containers = nil + } + if len(images) > 0 { chanValue := <-lock defer func() { lock <- chanValue }() - handle.fn(images) + handle.updateImages(images) + } else if len(containers) > 0 { + chanValue := <-lock + defer func() { lock <- chanValue }() + handle.updateContainers(containers) } else { select { case chanValue := <-lock: defer func() { lock <- chanValue }() - handle.fn(images) + handle.updateImages(images) default: log.Debug("Skipped. Another update already running.") } diff --git a/web/src/components/ContainerView.tsx b/web/src/components/ContainerView.tsx index 4e6162d..0edc90e 100644 --- a/web/src/components/ContainerView.tsx +++ b/web/src/components/ContainerView.tsx @@ -6,22 +6,19 @@ import Spinner from "./Spinner"; import SpinnerModal from "./SpinnerModal"; import { UpdateSelected, UpdateAll, UpdateCheck } from "./UpdateButtons"; -interface ViewModel { - Containers: ContainerModel[]; -} - const ContainerView = () => { const [loading, setLoading] = useState(true); const [checking, setChecking] = useState(false); const [updating, setUpdating] = useState(false); + const [updatingImage, setUpdatingContainer] = useState(null); const [hasChecked, setHasChecked] = useState(false); - const [viewModel, setViewModel] = useState({ Containers: [] }); + const [containers, setContainers] = useState([]); - const containers = viewModel.Containers; const containersWithUpdates = containers.filter((c) => c.HasUpdate); const containersWithoutUpdates = containers.filter((c) => !c.HasUpdate); - const hasSelectedContainers = containers.some((c) => c.Selected); + const selectedContainers = containers.filter((c) => c.Selected); const hasUpdates = containersWithUpdates.length > 0; + const hasSelectedContainers = selectedContainers.length > 0; const checkForUpdates = async (containersToUpdate?: ContainerModel[]) => { @@ -31,33 +28,33 @@ const ContainerView = () => { setChecking(true); - setViewModel((m) => ({ - ...m, - Containers: m.Containers.map((c) => ({ + setContainers((current) => + current.map((c) => ({ ...c, IsChecking: true })) - })); + ); await Promise.all(containersToUpdate.map(async (c1) => { const result = await check(c1.ContainerID); - setViewModel((m) => ({ - ...m, - Containers: m.Containers.map((c2) => (c1.ContainerID === c2.ContainerID ? { + setContainers((current) => + current.map((c2) => (c1.ContainerID === c2.ContainerID ? { ...c2, ...result, IsChecking: false } : c2 )) - })); + ); })); setChecking(false); setHasChecked(true); }; - const mapListDataToViewModel = (data: ListResponse) => ({ - Containers: data.Containers.map((c) => ({ + const listContainers = async () => { + setLoading(true); + const data = await list(); + const mappedData = data.Containers.map((c) => ({ ...c, Selected: false, IsChecking: false, @@ -65,48 +62,42 @@ const ContainerView = () => { IsUpdating: false, NewVersion: "", NewVersionCreated: "" - })) - }); - - const listContainers = async () => { - setLoading(true); - const data = await list(); - const mappedViewModel = mapListDataToViewModel(data); - setViewModel((m) => ({ - ...m, - ...mappedViewModel })); + setContainers(mappedData); setLoading(false); setHasChecked(false); - return mappedViewModel; + return mappedData; }; - const updateImages = async (imagesToUpdate?: string[]) => { + const updateImages = async (containersToUpdate: ContainerModel[]) => { setUpdating(true); - await update(imagesToUpdate); - const data = await listContainers(); - await checkForUpdates(data.Containers); + const containerNames = containersToUpdate.map((c) => c.ContainerName); + for (const containerName of containerNames) { + setUpdatingContainer(containerName); + await update([containerName]); + } + setUpdatingContainer(null); + const clist = await listContainers(); + await checkForUpdates(clist); setUpdating(false); }; const updateAll = async () => { - await updateImages(); + await updateImages(containersWithUpdates); }; const updateSelected = async () => { - const selectedImages = containers.filter((c) => c.Selected === true).map((c) => c.ImageNameShort); - await updateImages(selectedImages); + await updateImages(selectedContainers); }; const onContainerClick = (container: ContainerModel) => { - setViewModel((m) => ({ - ...m, - Containers: m.Containers.map((c2) => (container.ContainerID === c2.ContainerID ? { + setContainers((current) => + current.map((c2) => (container.ContainerID === c2.ContainerID ? { ...c2, - Selected: !c2.Selected, + Selected: !c2.Selected } : c2 )) - })); + ); }; useEffect(() => { @@ -115,7 +106,7 @@ const ContainerView = () => { return (
- +
{hasUpdates diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts index a43e9ef..3aaea3c 100644 --- a/web/src/services/Api.ts +++ b/web/src/services/Api.ts @@ -94,11 +94,11 @@ export const check = async (containerId: string): Promise => { return data as CheckResponse; }; -export const update = async (images?: string[]): Promise => { +export const update = async (containers?: string[]): Promise => { let updateUrl = new URL(apiBasePath + "update"); - if (images instanceof Array) { - images.map((image) => updateUrl.searchParams.append("image", image)); + if (containers instanceof Array) { + containers.map((container) => updateUrl.searchParams.append("container", container)); } const response = await fetch(updateUrl.toString(), {