mirror of
https://github.com/containrrr/watchtower.git
synced 2026-01-30 04:35:15 +01:00
Only update the selected containers
This commit is contained in:
parent
ee9475bcda
commit
cdde01709c
4 changed files with 63 additions and 52 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(), {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue