Merge pull request #2 from linuxserver/sync_removal

Sync removal
This commit is contained in:
Josh Stark 2019-04-26 10:41:49 +01:00 committed by GitHub
commit 1ac46d3380
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 589 additions and 16 deletions

View File

@ -11,7 +11,7 @@ repositories {
mavenCentral()
}
version = '1.2.1'
version = '1.3.0'
sourceSets {

View File

@ -130,7 +130,8 @@ public class FleetApp {
*/
path("/api/v1", () -> {
get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer());
get("/images", new AllImagesApi(beans.getRepositoryDelegate(), beans.getImageDelegate()), new JsonTransformer());
get("/pullHistory", new GetImagePullHistoryApi(beans.getImageDelegate()), new JsonTransformer());
after("/*", (request, response) -> {

View File

@ -22,6 +22,7 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.ImagePullStat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,9 +30,7 @@ import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableLong;
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
import static io.linuxserver.fleet.db.dao.Utils.*;
public class DefaultImageDAO implements ImageDAO {
@ -46,9 +45,11 @@ public class DefaultImageDAO implements ImageDAO {
@Override
public Image findImageByRepositoryAndImageName(int repositoryId, String imageName) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_GetByName(?,?)}");
call = connection.prepareCall("{CALL Image_GetByName(?,?)}");
call.setInt(1, repositoryId);
call.setString(2, imageName);
@ -58,6 +59,8 @@ public class DefaultImageDAO implements ImageDAO {
} catch (SQLException e) {
LOGGER.error("Unable to fetch image", e);
} finally {
safeClose(call);
}
return null;
@ -68,9 +71,11 @@ public class DefaultImageDAO implements ImageDAO {
LOGGER.debug("Fetching image by ID: " + id);
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_Get(?)}");
call = connection.prepareCall("{CALL Image_Get(?)}");
call.setInt(1, id);
ResultSet results = call.executeQuery();
@ -79,6 +84,8 @@ public class DefaultImageDAO implements ImageDAO {
} catch (SQLException e) {
LOGGER.error("Unable to fetch image.", e);
} finally {
safeClose(call);
}
return null;
@ -89,9 +96,11 @@ public class DefaultImageDAO implements ImageDAO {
List<Image> images = new ArrayList<>();
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_GetAll(?,?)}");
call = connection.prepareCall("{CALL Image_GetAll(?,?)}");
call.setInt(1, repositoryId);
call.registerOutParameter(2, Types.INTEGER);
@ -104,6 +113,8 @@ public class DefaultImageDAO implements ImageDAO {
} catch (SQLException e) {
LOGGER.error("Unable to get all images", e);
} finally {
safeClose(call);
}
return new LimitedResult<>(images, images.size());
@ -112,9 +123,11 @@ public class DefaultImageDAO implements ImageDAO {
@Override
public InsertUpdateResult<Image> saveImage(Image image) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?)");
call = connection.prepareCall("{CALL Image_Save(?,?,?,?,?,?,?,?,?,?,?,?,?)");
setNullableInt(call, 1, image.getId());
call.setInt(2, image.getRepositoryId());
call.setString(3, image.getName());
@ -145,24 +158,70 @@ public class DefaultImageDAO implements ImageDAO {
LOGGER.error("Unable to save image", e);
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save image");
} finally {
safeClose(call);
}
}
@Override
public void removeImage(Integer id) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
PreparedStatement call = connection.prepareStatement("DELETE FROM Images WHERE `id` = ?");
call = connection.prepareCall("{CALL Image_Delete(?)}");
call.setInt(1, id);
call.executeUpdate();
call.close();
} catch (SQLException e) {
LOGGER.error("Error when removing image", e);
} finally {
safeClose(call);
}
}
@Override
public List<ImagePullStat> fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode) {
List<ImagePullStat> pullHistory = new ArrayList<>();
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
call = connection.prepareCall("CALL Image_GetPullHistory(?, ?)");
call.setInt(1, imageId);
call.setString(2, groupMode.toString());
ResultSet results = call.executeQuery();
while (results.next())
pullHistory.add(parseImagePullHistoryFromResultSet(results, groupMode));
call.close();
} catch (SQLException e) {
LOGGER.error("Error when fetching image pull history", e);
} finally {
safeClose(call);
}
return pullHistory;
}
private ImagePullStat parseImagePullHistoryFromResultSet(ResultSet results, ImagePullStat.GroupMode groupMode) throws SQLException {
return new ImagePullStat(
results.getInt("ImageId"),
results.getString("TimeGroup"),
results.getLong("ImagePulls"),
groupMode
);
}
private Image parseImageFromResultSet(ResultSet results) throws SQLException {
Image image = new Image(

View File

@ -28,6 +28,7 @@ import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import static io.linuxserver.fleet.db.dao.Utils.safeClose;
import static io.linuxserver.fleet.db.dao.Utils.setNullableInt;
import static io.linuxserver.fleet.db.dao.Utils.setNullableString;
@ -44,9 +45,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
@Override
public Repository fetchRepository(int id) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Get(?)}");
call = connection.prepareCall("{CALL Repository_Get(?)}");
call.setInt(1, id);
ResultSet results = call.executeQuery();
@ -55,6 +58,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
} catch (SQLException e) {
LOGGER.error("Unable to retrieve repository", e);
} finally {
safeClose(call);
}
return null;
@ -63,9 +68,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
@Override
public InsertUpdateResult<Repository> saveRepository(Repository repository) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}");
call = connection.prepareCall("{CALL Repository_Save(?,?,?,?,?,?,?)}");
setNullableInt(call, 1, repository.getId());
call.setString(2, repository.getName());
setNullableString(call, 3, repository.getVersionMask());
@ -90,6 +97,9 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
LOGGER.error("Unable to save repository", e);
return new InsertUpdateResult<>(null, InsertUpdateStatus.OK, "Unable to save repository");
} finally {
safeClose(call);
}
}
@ -98,9 +108,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
List<Repository> repositories = new ArrayList<>();
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_GetAll()}");
call = connection.prepareCall("{CALL Repository_GetAll()}");
ResultSet results = call.executeQuery();
while (results.next())
@ -108,6 +120,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
} catch (SQLException e) {
LOGGER.error("Unable to get all repositories", e);
} finally {
safeClose(call);
}
return repositories;
@ -116,9 +130,11 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
@Override
public Repository findRepositoryByName(String name) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_GetByName(?)}");
call = connection.prepareCall("{CALL Repository_GetByName(?)}");
call.setString(1, name);
ResultSet results = call.executeQuery();
@ -127,6 +143,8 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
} catch (SQLException e) {
LOGGER.error("Unable to retrieve repository", e);
} finally {
safeClose(call);
}
return null;
@ -135,15 +153,19 @@ public class DefaultRepositoryDAO implements RepositoryDAO {
@Override
public void removeRepository(int id) {
CallableStatement call = null;
try (Connection connection = databaseConnection.getConnection()) {
CallableStatement call = connection.prepareCall("{CALL Repository_Delete(?)}");
call = connection.prepareCall("{CALL Repository_Delete(?)}");
call.setInt(1, id);
call.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Error when removing repository", e);
} finally {
safeClose(call);
}
}

View File

@ -3,6 +3,9 @@ package io.linuxserver.fleet.db.dao;
import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.LimitedResult;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.ImagePullStat;
import java.util.List;
public interface ImageDAO {
@ -15,4 +18,6 @@ public interface ImageDAO {
InsertUpdateResult<Image> saveImage(Image image);
void removeImage(Integer id);
List<ImagePullStat> fetchImagePullHistory(Integer imageId, ImagePullStat.GroupMode groupMode);
}

View File

@ -49,4 +49,16 @@ class Utils {
else
call.setString(position, value);
}
static void safeClose(CallableStatement call) {
try {
if (null != call)
call.close();
} catch (SQLException e) {
throw new RuntimeException("Unable to close call", e);
}
}
}

View File

@ -22,6 +22,7 @@ import io.linuxserver.fleet.db.query.InsertUpdateResult;
import io.linuxserver.fleet.db.query.InsertUpdateStatus;
import io.linuxserver.fleet.exception.SaveException;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.ImagePullStat;
import java.util.List;
@ -65,4 +66,8 @@ public class ImageDelegate {
throw new SaveException(result.getStatusMessage());
}
public List<ImagePullStat> fetchImagePullHistory(int id, ImagePullStat.GroupMode groupMode) {
return imageDAO.fetchImagePullHistory(id, groupMode);
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model;
public class ImagePullStat {
private final int imageId;
private final String timeGroup;
private final long pullCount;
private final GroupMode groupMode;
public ImagePullStat(int imageId, String timeGroup, long pullCount, GroupMode groupMode) {
this.imageId = imageId;
this.timeGroup = timeGroup;
this.pullCount = pullCount;
this.groupMode = groupMode;
}
public int getImageId() {
return imageId;
}
public String getTimeGroup() {
return timeGroup;
}
public long getPullCount() {
return pullCount;
}
public GroupMode getGroupMode() {
return groupMode;
}
public enum GroupMode {
HOUR, DAY, WEEK, MONTH, YEAR;
@Override
public String toString() {
return name().toLowerCase();
}
public static boolean isValid(String value) {
try {
if (null == value) {
return false;
}
valueOf(value);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.model.api;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.ImagePullStat;
import java.util.ArrayList;
import java.util.List;
public class ApiImagePullHistory {
private int imageId;
private String imageName;
private String groupMode;
private List<ApiImagePullStat> pullHistory = new ArrayList<>();
public int getImageId() {
return imageId;
}
public String getImageName() {
return imageName;
}
public String getGroupMode() {
return groupMode;
}
public List<ApiImagePullStat> getPullHistory() {
return pullHistory;
}
public static ApiImagePullHistory fromPullStats(Image image, List<ImagePullStat> stats) {
ApiImagePullHistory history = new ApiImagePullHistory();
history.imageId = image.getId();
history.imageName = image.getName();
history.groupMode = stats.get(0).getGroupMode().toString();
for (ImagePullStat stat : stats)
history.pullHistory.add(new ApiImagePullStat(stat.getTimeGroup(), stat.getPullCount()));
return history;
}
public static class ApiImagePullStat {
private final String timeGroup;
private final long pullCount;
ApiImagePullStat(String timeGroup, long pullCount) {
this.timeGroup = timeGroup;
this.pullCount = pullCount;
}
public String getTimeGroup() {
return timeGroup;
}
public long getPullCount() {
return pullCount;
}
}
}

View File

@ -31,6 +31,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class DefaultSynchronisationState implements SynchronisationState {
@ -86,7 +87,9 @@ public class DefaultSynchronisationState implements SynchronisationState {
try {
List<String> repositories = context.getDockerHubDelegate().fetchAllRepositories();
onRepositoryScanned(context, repositories);
checkAndRemoveMissingRepositories(repositories, context);
for (String repositoryName : repositories)
synchroniseRepository(repositoryName, context);
@ -122,6 +125,7 @@ public class DefaultSynchronisationState implements SynchronisationState {
if (repository.isSyncEnabled()) {
List<DockerHubImage> images = context.getDockerHubDelegate().fetchAllImagesFromRepository(repository.getName());
checkAndRemoveMissingImages(repository, images, context);
int totalSize = images.size();
LOGGER.info("Found {} images in Docker Hub", totalSize);
@ -139,7 +143,7 @@ public class DefaultSynchronisationState implements SynchronisationState {
image.withPullCount(dockerHubImage.getPullCount()).withVersion(maskedVersion);
context.getImageDelegate().saveImage(image);
onImageUpdated(context, new ImageUpdateEvent(image, i, totalSize));
onImageUpdated(context, new ImageUpdateEvent(image, i + 1, totalSize));
} catch (SaveException e) {
LOGGER.error("Unable to save updated image", e);
@ -151,6 +155,34 @@ public class DefaultSynchronisationState implements SynchronisationState {
}
}
private void checkAndRemoveMissingRepositories(List<String> repositories, SynchronisationContext context) {
LOGGER.info("Checking for any removed repositories.");
for (Repository storedRepository : context.getRepositoryDelegate().fetchAllRepositories()) {
if (!repositories.contains(storedRepository.getName())) {
LOGGER.info("Found repository which no longer exists in Docker Hub. Removing {}", storedRepository.getName());
context.getRepositoryDelegate().removeRepository(storedRepository.getId());
}
}
}
private void checkAndRemoveMissingImages(Repository repository, List<DockerHubImage> images, SynchronisationContext context) {
List<String> dockerHubImageNames = images.stream().map(DockerHubImage::getName).collect(Collectors.toList());
LOGGER.info("Checking for any removed images under {}", repository.getName());
for (Image storedImage : context.getImageDelegate().fetchImagesByRepository(repository.getId())) {
if (!dockerHubImageNames.contains(storedImage.getName())) {
LOGGER.info("Found image which no longer exists in Docker Hub. Removing {}", storedImage.getName());
context.getImageDelegate().removeImage(storedImage.getId());
}
}
}
private String getVersionMask(String repositoryMask, String imageMask) {
return imageMask == null ? repositoryMask : imageMask;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.web.routes;
import io.linuxserver.fleet.delegate.ImageDelegate;
import io.linuxserver.fleet.model.Image;
import io.linuxserver.fleet.model.ImagePullStat;
import io.linuxserver.fleet.model.api.ApiImagePullHistory;
import io.linuxserver.fleet.model.api.ApiResponse;
import io.linuxserver.fleet.model.api.FleetApiException;
import spark.Request;
import spark.Response;
import spark.Route;
import java.util.List;
public class GetImagePullHistoryApi implements Route {
private final ImageDelegate imageDelegate;
public GetImagePullHistoryApi(ImageDelegate imageDelegate) {
this.imageDelegate = imageDelegate;
}
@Override
public Object handle(Request request, Response response) {
String imageIdParam = request.queryParams("imageId");
if (null == imageIdParam) {
throw new FleetApiException(400, "Missing imageId param");
}
int imageId = Integer.parseInt(imageIdParam);
List<ImagePullStat> imagePullStats = imageDelegate.fetchImagePullHistory(imageId, getGroupMode(request));
Image image = imageDelegate.fetchImage(imageId);
return new ApiResponse<>("OK", ApiImagePullHistory.fromPullStats(image, imagePullStats));
}
private ImagePullStat.GroupMode getGroupMode(Request request) {
String groupMode = request.queryParams("groupMode");
return ImagePullStat.GroupMode.isValid(groupMode) ? ImagePullStat.GroupMode.valueOf(groupMode) : ImagePullStat.GroupMode.DAY;
}
}

View File

@ -0,0 +1,220 @@
DELIMITER //
CREATE TABLE ImagePullHistory (
`image_id` INT NOT NULL,
`pull_timestamp` BIGINT NOT NULL,
`pull_count` BIGINT NOT NULL,
PRIMARY KEY (`image_id`, `pull_timestamp`)
) ENGINE=InnoDB;
//
CREATE PROCEDURE `Image_SavePullHistory`
(
in_image_id INT,
in_image_pulls BIGINT,
OUT out_status INT,
OUT out_message VARCHAR(100)
)
BEGIN
IF EXISTS(SELECT 1 FROM Images WHERE `id` = in_image_id) THEN
INSERT INTO ImagePullHistory
(
`image_id`,
`pull_timestamp`,
`pull_count`
)
VALUES
(
in_image_id,
UNIX_TIMESTAMP(NOW()),
in_image_pulls
);
END IF;
SET out_status = 0;
SET out_message = "OK";
END;
//
CREATE PROCEDURE `Image_GetPullHistory`
(
in_image_id INT,
in_grouping_mode ENUM('hour', 'day', 'week', 'month', 'year')
)
BEGIN
IF in_grouping_mode = 'hour' THEN
SELECT
`image_id` AS ImageId,
MAX(`pull_count`) AS ImagePulls,
FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d%H') AS TimeGroup
FROM
ImagePullHistory
WHERE
`image_id` = in_image_id
GROUP BY `image_id`, TimeGroup
ORDER BY TimeGroup;
ELSEIF in_grouping_mode = 'day' THEN
SELECT
`image_id` AS ImageId,
MAX(`pull_count`) AS ImagePulls,
FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d') AS TimeGroup
FROM
ImagePullHistory
WHERE
`image_id` = in_image_id
GROUP BY `image_id`, TimeGroup
ORDER BY TimeGroup;
ELSEIF in_grouping_mode = 'week' THEN
SELECT
`image_id` AS ImageId,
MAX(`pull_count`) AS ImagePulls,
FROM_UNIXTIME(`pull_timestamp`, '%Y%v') AS TimeGroup
FROM
ImagePullHistory
WHERE
`image_id` = in_image_id
GROUP BY `image_id`, TimeGroup
ORDER BY TimeGroup;
ELSEIF in_grouping_mode = 'month' THEN
SELECT
`image_id` AS ImageId,
MAX(`pull_count`) AS ImagePulls,
FROM_UNIXTIME(`pull_timestamp`, '%Y%m') AS TimeGroup
FROM
ImagePullHistory
WHERE
`image_id` = in_image_id
GROUP BY `image_id`, TimeGroup
ORDER BY TimeGroup;
ELSEIF in_grouping_mode = 'year' THEN
SELECT
`image_id` AS ImageId,
MAX(`pull_count`) AS ImagePulls,
FROM_UNIXTIME(`pull_timestamp`, '%Y') AS TimeGroup
FROM
ImagePullHistory
WHERE
`image_id` = in_image_id
GROUP BY `image_id`, TimeGroup
ORDER BY TimeGroup;
END IF;
END;
//
DROP PROCEDURE `Image_Save`//
CREATE PROCEDURE `Image_Save`
(
in_id INT,
in_repository INT,
in_name VARCHAR(255),
in_pull_count BIGINT,
in_version VARCHAR(100),
in_version_mask VARCHAR(255),
in_hidden TINYINT,
in_unstable TINYINT,
in_deprecated TINYINT,
in_deprecation_reason VARCHAR(255),
OUT out_id INT,
OUT out_status INT,
OUT out_message VARCHAR(100)
)
BEGIN
IF in_id IS NULL THEN
INSERT INTO Images
(
`repository`,
`name`,
`pulls`,
`latest_version`,
`version_mask`,
`hidden`,
`unstable`,
`deprecated`,
`deprecation_reason`
)
VALUES
(
in_repository,
in_name,
in_pull_count,
in_version,
in_version_mask,
in_hidden,
in_unstable,
in_deprecated,
in_deprecation_reason
);
SET out_id = LAST_INSERT_ID();
SET out_status = 0;
SET out_message = 'OK';
ELSE
UPDATE Images
SET
`name` = in_name,
`pulls` = in_pull_count,
`latest_version` = in_version,
`version_mask` = in_version_mask,
`hidden` = in_hidden,
`unstable` = in_unstable,
`deprecated` = in_deprecated,
`deprecation_reason` = in_deprecation_reason
WHERE
`id` = in_id;
SET out_id = in_id;
CALL Image_SavePullHistory(out_id, in_pull_count, out_status, out_message);
END IF;
END;
//
DROP PROCEDURE `Repository_Delete`//
CREATE PROCEDURE `Repository_Delete`
(
in_id INT
)
BEGIN
DELETE FROM ImagePullHistory WHERE `image_id` IN (SELECT `id` FROM Images WHERE `repository` = in_id);
DELETE FROM Images WHERE `repository` = in_id;
DELETE FROM Repositories WHERE `id` = in_id;
END;
//
CREATE PROCEDURE `Image_Delete`
(
in_id INT
)
BEGIN
DELETE FROM ImagePullHistory WHERE `image_id` = in_id;
DELETE FROM Images WHERE `id` = in_id;
END;
//