Only update the selected containers

This commit is contained in:
Anders Roos 2022-11-21 20:45:05 +01:00
parent ee9475bcda
commit cdde01709c
4 changed files with 63 additions and 52 deletions

View file

@ -190,7 +190,10 @@ func Run(c *cobra.Command, names []string) {
httpAPI := api.New(apiToken) httpAPI := api.New(apiToken)
if enableUpdateAPI { 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) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
// If polling isn't enabled the scheduler is never started and // If polling isn't enabled the scheduler is never started and
// we need to trigger the startup messages manually. // we need to trigger the startup messages manually.

View file

@ -14,7 +14,7 @@ var (
) )
// New is a factory function creating a new Handler instance // 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 { if updateLock != nil {
lock = updateLock lock = updateLock
} else { } else {
@ -23,15 +23,17 @@ func New(updateFn func(images []string), updateLock chan bool) *Handler {
} }
return &Handler{ return &Handler{
fn: updateFn, updateImages: updateImages,
Path: "/v1/update", updateContainers: updateContainers,
Path: "/v1/update",
} }
} }
// Handler is an API handler used for triggering container update scans // Handler is an API handler used for triggering container update scans
type Handler struct { type Handler struct {
fn func(images []string) updateImages func(images []string)
Path string updateContainers func(containerNames []string)
Path string
} }
// Handle is the actual http.Handle function doing all the heavy lifting // 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 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 { if len(images) > 0 {
chanValue := <-lock chanValue := <-lock
defer func() { lock <- chanValue }() 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 { } else {
select { select {
case chanValue := <-lock: case chanValue := <-lock:
defer func() { lock <- chanValue }() defer func() { lock <- chanValue }()
handle.fn(images) handle.updateImages(images)
default: default:
log.Debug("Skipped. Another update already running.") log.Debug("Skipped. Another update already running.")
} }

View file

@ -6,22 +6,19 @@ import Spinner from "./Spinner";
import SpinnerModal from "./SpinnerModal"; import SpinnerModal from "./SpinnerModal";
import { UpdateSelected, UpdateAll, UpdateCheck } from "./UpdateButtons"; import { UpdateSelected, UpdateAll, UpdateCheck } from "./UpdateButtons";
interface ViewModel {
Containers: ContainerModel[];
}
const ContainerView = () => { const ContainerView = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [checking, setChecking] = useState(false); const [checking, setChecking] = useState(false);
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
const [updatingImage, setUpdatingContainer] = useState<string | null>(null);
const [hasChecked, setHasChecked] = useState(false); const [hasChecked, setHasChecked] = useState(false);
const [viewModel, setViewModel] = useState<ViewModel>({ Containers: [] }); const [containers, setContainers] = useState<ContainerModel[]>([]);
const containers = viewModel.Containers;
const containersWithUpdates = containers.filter((c) => c.HasUpdate); const containersWithUpdates = containers.filter((c) => c.HasUpdate);
const containersWithoutUpdates = 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 hasUpdates = containersWithUpdates.length > 0;
const hasSelectedContainers = selectedContainers.length > 0;
const checkForUpdates = async (containersToUpdate?: ContainerModel[]) => { const checkForUpdates = async (containersToUpdate?: ContainerModel[]) => {
@ -31,33 +28,33 @@ const ContainerView = () => {
setChecking(true); setChecking(true);
setViewModel((m) => ({ setContainers((current) =>
...m, current.map((c) => ({
Containers: m.Containers.map((c) => ({
...c, ...c,
IsChecking: true IsChecking: true
})) }))
})); );
await Promise.all(containersToUpdate.map(async (c1) => { await Promise.all(containersToUpdate.map(async (c1) => {
const result = await check(c1.ContainerID); const result = await check(c1.ContainerID);
setViewModel((m) => ({ setContainers((current) =>
...m, current.map((c2) => (c1.ContainerID === c2.ContainerID ? {
Containers: m.Containers.map((c2) => (c1.ContainerID === c2.ContainerID ? {
...c2, ...c2,
...result, ...result,
IsChecking: false IsChecking: false
} : c2 } : c2
)) ))
})); );
})); }));
setChecking(false); setChecking(false);
setHasChecked(true); setHasChecked(true);
}; };
const mapListDataToViewModel = (data: ListResponse) => ({ const listContainers = async () => {
Containers: data.Containers.map((c) => ({ setLoading(true);
const data = await list();
const mappedData = data.Containers.map((c) => ({
...c, ...c,
Selected: false, Selected: false,
IsChecking: false, IsChecking: false,
@ -65,48 +62,42 @@ const ContainerView = () => {
IsUpdating: false, IsUpdating: false,
NewVersion: "", NewVersion: "",
NewVersionCreated: "" NewVersionCreated: ""
}))
});
const listContainers = async () => {
setLoading(true);
const data = await list();
const mappedViewModel = mapListDataToViewModel(data);
setViewModel((m) => ({
...m,
...mappedViewModel
})); }));
setContainers(mappedData);
setLoading(false); setLoading(false);
setHasChecked(false); setHasChecked(false);
return mappedViewModel; return mappedData;
}; };
const updateImages = async (imagesToUpdate?: string[]) => { const updateImages = async (containersToUpdate: ContainerModel[]) => {
setUpdating(true); setUpdating(true);
await update(imagesToUpdate); const containerNames = containersToUpdate.map((c) => c.ContainerName);
const data = await listContainers(); for (const containerName of containerNames) {
await checkForUpdates(data.Containers); setUpdatingContainer(containerName);
await update([containerName]);
}
setUpdatingContainer(null);
const clist = await listContainers();
await checkForUpdates(clist);
setUpdating(false); setUpdating(false);
}; };
const updateAll = async () => { const updateAll = async () => {
await updateImages(); await updateImages(containersWithUpdates);
}; };
const updateSelected = async () => { const updateSelected = async () => {
const selectedImages = containers.filter((c) => c.Selected === true).map((c) => c.ImageNameShort); await updateImages(selectedContainers);
await updateImages(selectedImages);
}; };
const onContainerClick = (container: ContainerModel) => { const onContainerClick = (container: ContainerModel) => {
setViewModel((m) => ({ setContainers((current) =>
...m, current.map((c2) => (container.ContainerID === c2.ContainerID ? {
Containers: m.Containers.map((c2) => (container.ContainerID === c2.ContainerID ? {
...c2, ...c2,
Selected: !c2.Selected, Selected: !c2.Selected
} : c2 } : c2
)) ))
})); );
}; };
useEffect(() => { useEffect(() => {
@ -115,7 +106,7 @@ const ContainerView = () => {
return ( return (
<main className="mt-5 p-5 d-block"> <main className="mt-5 p-5 d-block">
<SpinnerModal visible={updating} title="Updating containers" message="Please wait..." /> <SpinnerModal visible={updating} title={`Updating ${updatingImage ?? "containers"}`} message="Please wait..." />
<div className="row mb-2"> <div className="row mb-2">
<div className="col-12 col-md-4 d-flex align-items-center"> <div className="col-12 col-md-4 d-flex align-items-center">
{hasUpdates {hasUpdates

View file

@ -94,11 +94,11 @@ export const check = async (containerId: string): Promise<CheckResponse> => {
return data as CheckResponse; return data as CheckResponse;
}; };
export const update = async (images?: string[]): Promise<boolean> => { export const update = async (containers?: string[]): Promise<boolean> => {
let updateUrl = new URL(apiBasePath + "update"); let updateUrl = new URL(apiBasePath + "update");
if (images instanceof Array) { if (containers instanceof Array) {
images.map((image) => updateUrl.searchParams.append("image", image)); containers.map((container) => updateUrl.searchParams.append("container", container));
} }
const response = await fetch(updateUrl.toString(), { const response = await fetch(updateUrl.toString(), {