Core Meta

- Store core meta as part of standard image save
  - Display core meta on main image display
  - TODO: upload image and save.
This commit is contained in:
Josh Stark 2020-01-18 17:31:21 +00:00
parent cd0f025d55
commit b8f64bd4f2
18 changed files with 621 additions and 112 deletions

View File

@ -41,7 +41,7 @@ public class DockerImageUpdateResponse implements AsyncDockerApiResponse {
@Override
public void handleDockerApiResponse() {
controller.getImageService().applyImageUpdate(imageKey, latestImage);
controller.getImageService().applyImageUpstreamUpdate(imageKey, latestImage);
}
@Override

View File

@ -28,12 +28,14 @@ import io.linuxserver.fleet.v2.types.*;
import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest;
import io.linuxserver.fleet.v2.types.meta.ImageCoreMeta;
import io.linuxserver.fleet.v2.types.meta.ImageMetaData;
import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec;
import io.linuxserver.fleet.v2.types.meta.history.ImagePullHistory;
import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic;
import io.linuxserver.fleet.v2.types.meta.template.ImageTemplateHolder;
import java.net.URL;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
@ -50,7 +52,7 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO {
private static final String CreateRepositoryOutline = "{CALL Repository_CreateOutline(?,?,?,?,?,?,?,?)}";
private static final String StoreRepository = "{CALL Repository_Store(?,?,?,?)}";
private static final String StoreImage = "{CALL Image_Store(?,?,?,?,?,?,?,?,?,?,?)}";
private static final String StoreImage = "{CALL Image_Store(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)}";
private static final String CreateTagBranchOutline = "{CALL Image_CreateTagBranchOutline(?,?)}";
private static final String StoreTagBranch = "{CALL Image_StoreTagBranch(?,?,?,?)}";
private static final String StoreTagDigest = "{CALL Image_StoreTagDigest(?,?,?,?,?)}";
@ -103,6 +105,12 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO {
call.setBoolean(i++, image.isSyncEnabled());
Utils.setNullableString(call, i++, image.getVersionMask());
Utils.setNullableString(call, i++, image.getMetaData().getCategory());
Utils.setNullableString(call, i++, image.getMetaData().getSupportUrl());
Utils.setNullableString(call, i++, image.getMetaData().getAppUrl());
Utils.setNullableString(call, i++, image.getMetaData().getBaseImage());
Utils.setNullableString(call, i++, image.getMetaData().getAppImagePath());
call.registerOutParameter(i, Types.VARCHAR);
final ResultSet results = call.executeQuery();
@ -486,7 +494,7 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO {
final Image image = new Image(imageKey,
makeSyncSpec(results),
makeImageMetaData(connection, imageKey),
makeImageMetaData(connection, imageKey, results),
makeCountData(results),
results.getString("Description"),
results.getTimestamp("LastUpdated").toLocalDateTime());
@ -499,12 +507,22 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO {
return null;
}
private ImageMetaData makeImageMetaData(final Connection connection, final ImageKey imageKey) throws SQLException {
private ImageMetaData makeImageMetaData(final Connection connection, final ImageKey imageKey, final ResultSet mainImageResults) throws SQLException {
return new ImageMetaData(makePullHistory(connection, imageKey),
return new ImageMetaData(makeCoreMeta(mainImageResults),
makePullHistory(connection, imageKey),
templateFactory.makeTemplateHolder(connection, imageKey));
}
private ImageCoreMeta makeCoreMeta(final ResultSet mainImageResults) throws SQLException {
return new ImageCoreMeta(mainImageResults.getString("CoreMetaImagePath"),
mainImageResults.getString("CoreMetaBaseImage"),
mainImageResults.getString("CoreMetaCategory"),
mainImageResults.getString("CoreMetaSupportUrl"),
mainImageResults.getString("CoreMetaAppUrl"));
}
private ImagePullHistory makePullHistory(final Connection connection, final ImageKey imageKey) throws SQLException {
final ImagePullHistory pullHistory = new ImagePullHistory();

View File

@ -28,14 +28,14 @@ import io.linuxserver.fleet.v2.service.util.TemplateMerger;
import io.linuxserver.fleet.v2.types.*;
import io.linuxserver.fleet.v2.types.docker.DockerImage;
import io.linuxserver.fleet.v2.types.docker.DockerTag;
import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest;
import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.*;
import io.linuxserver.fleet.v2.types.meta.ImageCoreMeta;
import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -192,7 +192,7 @@ public class ImageService {
return getAllRepositories().stream().filter(r -> !r.isHidden()).collect(Collectors.toList());
}
public Image applyImageUpdate(final ImageKey imageKey, final DockerImage latestImage) {
public Image applyImageUpstreamUpdate(final ImageKey imageKey, final DockerImage latestImage) {
final Image cachedImage = findImage(imageKey);
final Image cloned = cachedImage.cloneForUpdate(latestImage.getPullCount(),
@ -240,6 +240,25 @@ public class ImageService {
storeImage(updatableClone);
}
public void updateImageGeneralInfo(final ImageKey imageKey, final ImageGeneralInfoUpdateRequest generalInfoUpdateRequest) {
final Image image = findImage(imageKey);
String appLogoPath = image.getMetaData().getAppImagePath();
if (null != generalInfoUpdateRequest.getImageAppLogo()) {
// TODO: Write FileManager
}
final ImageCoreMeta coreMeta = new ImageCoreMeta(appLogoPath,
generalInfoUpdateRequest.getBaseImage(),
generalInfoUpdateRequest.getCategory(),
generalInfoUpdateRequest.getSupportUrl(),
generalInfoUpdateRequest.getApplicationUrl());
final Image cloned = image.cloneWithMetaData(image.getMetaData().cloneWithCoreMeta(coreMeta));
storeImage(cloned);
}
public void updateImageTemplate(final ImageKey imageKey, final ImageTemplateRequest imageTemplateUpdateFields) {
final Image image = findImage(imageKey);

View File

@ -48,6 +48,8 @@ public class SynchronisationService extends AbstractAppService {
if (repository.isSyncEnabled()) {
getLogger().info("synchroniseUpstreamRepository checking {} for new images since last sync", repository);
final List<DockerImage> apiImages = getController().getConfiguredDockerDelegate().getImagesForRepository(repository.getKey());
for (DockerImage apiImage : apiImages) {

View File

@ -31,11 +31,11 @@ public class TemplateMerger {
public final Image mergeTemplateRequestIntoImage(final Image image, final ImageTemplateRequest templateRequest) {
final ImageTemplateHolder templateHolder = makeTemplateHolder(templateRequest);
addMisc(templateRequest, templateHolder);
addPorts(templateRequest, templateHolder);
addVolumes(templateRequest, templateHolder);
addMisc( templateRequest, templateHolder);
addPorts( templateRequest, templateHolder);
addVolumes( templateRequest, templateHolder);
addEnvironment(templateRequest, templateHolder);
addDevices(templateRequest, templateHolder);
addDevices( templateRequest, templateHolder);
final Image cloned = image.cloneWithMetaData(image.getMetaData().cloneWithTemplate(templateHolder));
return cloned;

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) 2020 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.v2.types.internal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
public class AbstractParamRequest {
private final Map<String, List<String>> params;
public AbstractParamRequest(final Map<String, List<String>> params) {
this.params = params;
}
protected final List<String> getParams(final String key) {
return params.get(key);
}
protected final String getOrNull(final String value) {
return "".equalsIgnoreCase(value.trim()) ? null : value;
}
protected final String getFirstOrNull(final String key) {
final List<String> strings = params.get(key);
if (null == strings || strings.isEmpty()) {
return null;
}
return strings.get(0);
}
protected final boolean getAsBoolean(final String value) {
return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
}
@SafeVarargs
protected final void checkLists(final List<String>... lists) {
boolean containsDifferent = false;
boolean allNull = Stream.of(lists).allMatch(Objects::isNull);
boolean noneNull = Stream.of(lists).allMatch(Objects::nonNull);
if (allNull || noneNull) {
if (allNull) {
return;
}
} else {
containsDifferent = true;
}
if (!containsDifferent) {
int prevSize = -1;
for (List<String> list : lists) {
if (prevSize != -1 && list.size() != prevSize) {
containsDifferent = true;
break;
} else {
prevSize = list.size();
}
}
}
if (containsDifferent) {
throw new IllegalArgumentException("One or more values are null when others are not, or sizes mismatch");
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2020 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.v2.types.internal;
import io.linuxserver.fleet.v2.key.ImageKey;
import java.io.InputStream;
public class ImageAppLogo {
private final ImageKey imageKey;
private final InputStream rawDataStream;
private final String logoName;
private final long logoSize;
private final String fileExtension;
public ImageAppLogo(final ImageKey imageKey,
final InputStream rawDataStream,
final String logoName,
final long logoSize,
final String fileExtension) {
this.imageKey = imageKey;
this.rawDataStream = rawDataStream;
this.logoName = logoName;
this.logoSize = logoSize;
this.fileExtension = fileExtension;
}
public final ImageKey getImageKey() {
return imageKey;
}
public final InputStream getRawDataStream() {
return rawDataStream;
}
public final String getLogoName() {
return logoName;
}
public final long getLogoSize() {
return logoSize;
}
public final String getFileExtension() {
return fileExtension;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2020 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.v2.types.internal;
import java.util.List;
import java.util.Map;
public class ImageGeneralInfoUpdateRequest extends AbstractParamRequest {
private final ImageAppLogo imageAppLogo;
public ImageGeneralInfoUpdateRequest(final Map<String, List<String>> params,
final ImageAppLogo imageAppLogo) {
super(params);
this.imageAppLogo = imageAppLogo;
}
public final ImageAppLogo getImageAppLogo() {
return imageAppLogo;
}
public final String getBaseImage() {
return getFirstOrNull("ImageBase");
}
public final String getCategory() {
return getFirstOrNull("ImageCategory");
}
public final String getSupportUrl() {
return getFirstOrNull("ImageSupportUrl");
}
public final String getApplicationUrl() {
return getFirstOrNull("ImageApplicationUrl");
}
}

View File

@ -20,15 +20,11 @@ package io.linuxserver.fleet.v2.types.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
public class ImageTemplateRequest {
private final Map<String, List<String>> rawTemplateParams;
public class ImageTemplateRequest extends AbstractParamRequest {
public ImageTemplateRequest(final Map<String, List<String>> rawTemplateParams) {
this.rawTemplateParams = rawTemplateParams;
super(rawTemplateParams);
}
public final String getRegistryUrl() {
@ -48,14 +44,14 @@ public class ImageTemplateRequest {
}
public final List<String> getCapabilities() {
return rawTemplateParams.get("ImageTemplateCapabilities");
return getParams("ImageTemplateCapabilities");
}
public final List<TemplateItem<String>> getPorts() {
final List<String> portNumbers = rawTemplateParams.get("imageTemplatePort");
final List<String> portProtocols = rawTemplateParams.get("imageTemplatePortProtocol");
final List<String> portDescriptions = rawTemplateParams.get("imageTemplatePortDescription");
final List<String> portNumbers = getParams("imageTemplatePort");
final List<String> portProtocols = getParams("imageTemplatePortProtocol");
final List<String> portDescriptions = getParams("imageTemplatePortDescription");
checkLists(portNumbers, portProtocols, portDescriptions);
@ -77,9 +73,9 @@ public class ImageTemplateRequest {
public final List<TemplateItem<Boolean>> getVolumes() {
final List<String> volumeNames = rawTemplateParams.get("imageTemplateVolume");
final List<String> volumeReadOnlys = rawTemplateParams.get("imageTemplateVolumeReadonly");
final List<String> volumeDescriptions = rawTemplateParams.get("imageTemplateVolumeDescription");
final List<String> volumeNames = getParams("imageTemplateVolume");
final List<String> volumeReadOnlys = getParams("imageTemplateVolumeReadonly");
final List<String> volumeDescriptions = getParams("imageTemplateVolumeDescription");
checkLists(volumeNames, volumeReadOnlys, volumeDescriptions);
@ -101,8 +97,8 @@ public class ImageTemplateRequest {
public final List<TemplateItem<Void>> getEnvironment() {
final List<String> envNames = rawTemplateParams.get("imageTemplateEnv");
final List<String> envDescriptions = rawTemplateParams.get("imageTemplateEnvDescription");
final List<String> envNames = getParams("imageTemplateEnv");
final List<String> envDescriptions = getParams("imageTemplateEnvDescription");
checkLists(envNames, envDescriptions);
@ -121,8 +117,8 @@ public class ImageTemplateRequest {
public final List<TemplateItem<Void>> getDevices() {
final List<String> deviceNames = rawTemplateParams.get("imageTemplateDevice");
final List<String> deviceDescriptions = rawTemplateParams.get("imageTemplateDeviceDescription");
final List<String> deviceNames = getParams("imageTemplateDevice");
final List<String> deviceDescriptions = getParams("imageTemplateDeviceDescription");
checkLists(deviceNames, deviceDescriptions);
@ -139,62 +135,6 @@ public class ImageTemplateRequest {
return env;
}
private String getOrNull(final String value) {
return "".equalsIgnoreCase(value.trim()) ? null : value;
}
private String getFirstOrNull(final String key) {
final List<String> strings = rawTemplateParams.get(key);
if (null == strings || strings.isEmpty()) {
return null;
}
return strings.get(0);
}
private boolean getAsBoolean(final String value) {
return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
}
@SafeVarargs
private final void checkLists(final List<String>... lists) {
boolean containsDifferent = false;
boolean allNull = Stream.of(lists).allMatch(Objects::isNull);
boolean noneNull = Stream.of(lists).allMatch(Objects::nonNull);
if (allNull || noneNull) {
if (allNull) {
return;
}
} else {
containsDifferent = true;
}
if (!containsDifferent) {
int prevSize = -1;
for (List<String> list : lists) {
if (prevSize != -1 && list.size() != prevSize) {
containsDifferent = true;
break;
} else {
prevSize = list.size();
}
}
}
if (containsDifferent) {
throw new IllegalArgumentException("One or more values are null when others are not, or sizes mismatch");
}
}
public static class TemplateItem<T> {
private final String name;

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 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.v2.types.meta;
import java.net.URL;
public class ImageCoreMeta {
private final String appImagePath;
private final String baseImage;
private final String category;
private final String supportUrl;
private final String appUrl;
public ImageCoreMeta(final String appImagePath,
final String baseImage,
final String category,
final String supportUrl,
final String appUrl) {
this.appImagePath = appImagePath;
this.baseImage = baseImage;
this.category = category;
this.supportUrl = supportUrl;
this.appUrl = appUrl;
}
public final String getAppImagePath() {
return appImagePath;
}
public final String getBaseImage() {
return baseImage;
}
public final String getCategory() {
return category;
}
public final String getSupportUrl() {
return supportUrl;
}
public final String getAppUrl() {
return appUrl;
}
}

View File

@ -27,14 +27,22 @@ public class ImageMetaData {
private final ImagePullHistory pullHistory;
private final ImageTemplateHolder templateHolder;
private final ImageCoreMeta coreMeta;
public ImageMetaData(final ImagePullHistory pullHistory, final ImageTemplateHolder templateHolder) {
public ImageMetaData(final ImageCoreMeta coreMeta,
final ImagePullHistory pullHistory,
final ImageTemplateHolder templateHolder) {
this.coreMeta = coreMeta;
this.pullHistory = pullHistory;
this.templateHolder = templateHolder;
}
public final ImageMetaData cloneWithTemplate(final ImageTemplateHolder templateHolder) {
return new ImageMetaData(pullHistory, templateHolder);
return new ImageMetaData(getCoreMeta(), this.pullHistory, templateHolder);
}
public final ImageMetaData cloneWithCoreMeta(final ImageCoreMeta coreMeta) {
return new ImageMetaData(coreMeta, this.pullHistory, getTemplates());
}
public final List<ImagePullStatistic> getHistoryFor(final ImagePullStatistic.StatGroupMode groupMode) {
@ -44,4 +52,37 @@ public class ImageMetaData {
public final ImageTemplateHolder getTemplates() {
return templateHolder;
}
public final ImageCoreMeta getCoreMeta() {
return coreMeta;
}
public final String getAppImagePath() {
return getCoreMeta().getAppImagePath();
}
public final String getBaseImage() {
return getCoreMeta().getBaseImage();
}
public final String getCategory() {
return getCoreMeta().getCategory();
}
public final String getSupportUrl() {
return getCoreMeta().getSupportUrl();
}
public final String getAppUrl() {
return getCoreMeta().getAppUrl();
}
public final boolean isPopulated() {
return (
null != getCategory() ||
null != getSupportUrl() ||
null != getAppUrl()
);
}
}

View File

@ -18,10 +18,13 @@
package io.linuxserver.fleet.v2.web.routes;
import io.javalin.http.Context;
import io.javalin.http.UploadedFile;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.service.ImageService;
import io.linuxserver.fleet.v2.types.docker.DockerCapability;
import io.linuxserver.fleet.v2.types.internal.ImageAppLogo;
import io.linuxserver.fleet.v2.types.internal.ImageGeneralInfoUpdateRequest;
import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest;
import io.linuxserver.fleet.v2.web.PageModelSpec;
@ -78,9 +81,31 @@ public class AdminImageEditController extends AbstractPageHandler {
if (!ctx.isMultipartFormData()) {
throw new IllegalArgumentException("Form submission must be form/multipart");
}
imageService.updateImageGeneralInfo(imageKey, makeInfoRequest(imageKey, ctx));
}
private void handleTemplateUpdate(final Context ctx, final ImageKey imageKey) {
imageService.updateImageTemplate(imageKey, new ImageTemplateRequest(ctx.formParamMap()));
}
private ImageGeneralInfoUpdateRequest makeInfoRequest(final ImageKey imageKey, final Context ctx) {
return new ImageGeneralInfoUpdateRequest(ctx.formParamMap(),
makeImageLogoIfPresent(imageKey, ctx.uploadedFile("ImageAppLogo")));
}
private ImageAppLogo makeImageLogoIfPresent(final ImageKey imageKey, final UploadedFile uploadedFile) {
if (null != uploadedFile) {
return new ImageAppLogo(imageKey,
uploadedFile.getContent(),
uploadedFile.getFilename(),
uploadedFile.getSize(),
uploadedFile.getExtension());
}
return null;
}
}

View File

@ -0,0 +1,145 @@
DELIMITER //
CREATE OR REPLACE VIEW `Image_View` AS (
SELECT
-- Key
images.`id` AS `ImageId`,
images.`name` AS `ImageName`,
images.`repository` AS `RepositoryId`,
repositories.`name` AS `RepositoryName`,
-- Counts
images.`pulls` AS `LatestPullCount`,
images.`stars` AS `LatestStarCount`,
-- Spec
images.`sync_enabled` AS `SyncEnabled`,
images.`version_mask` AS `VersionMask`,
images.`hidden` AS `Hidden`,
images.`stable` AS `Stable`,
images.`deprecated` AS `Deprecated`,
-- General
images.`description` AS `Description`,
images.`modified` AS `LastUpdated`,
-- Core Meta
meta.icon_url AS `CoreMetaImagePath`,
meta.base_image AS `CoreMetaBaseImage`,
meta.category AS `CoreMetaCategory`,
meta.support AS `CoreMetaSupportUrl`,
meta.app_url AS `CoreMetaAppUrl`
FROM
Image images
JOIN
Repository repositories ON repositories.`id` = images.`repository`
LEFT JOIN
ImageMetadata meta on meta.`image_id` = images.`id`
);
//
CREATE OR REPLACE PROCEDURE `Image_Store`
(
in_id INT,
in_pulls BIGINT,
in_stars INT,
in_description TEXT,
in_modified TIMESTAMP,
in_deprecated TINYINT,
in_hidden TINYINT,
in_stable TINYINT,
in_synchronised TINYINT,
in_version_mask VARCHAR(255),
in_category VARCHAR(255),
in_support VARCHAR(500),
in_app_url VARCHAR(500),
in_base_image VARCHAR(255),
in_icon_url VARCHAR(1000),
OUT out_status enum('Updated', 'NoChange')
)
BEGIN
IF NOT EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN
SET out_status = 'NoChange';
ELSE
UPDATE
Image
SET
`pulls` = in_pulls,
`stars` = in_stars,
`description` = in_description,
`modified` = in_modified,
`deprecated` = in_deprecated,
`hidden` = in_hidden,
`stable` = in_stable,
`sync_enabled` = in_synchronised,
`version_mask` = in_version_mask
WHERE
`id` = in_id;
-- Only add core metadata if it has been provided with at least one value
IF
(
in_category IS NOT NULL OR
in_support IS NOT NULL OR
in_app_url IS NOT NULL OR
in_base_image IS NOT NULL OR
in_icon_url IS NOT NULL
)
THEN
IF NOT EXISTS(SELECT 1 FROM ImageMetadata WHERE `image_id` = in_id) THEN
INSERT INTO ImageMetadata
(
`image_id`,
`category`,
`support`,
`app_url`,
`base_image`,
`icon_url`
)
VALUES
(
in_id,
in_category,
in_support,
in_app_url,
in_base_image,
in_icon_url
);
ELSE
UPDATE ImageMetadata
SET
`category` = in_category,
`support` = in_support,
`app_url` = in_app_url,
`base_image` = in_base_image,
`icon_url` = in_icon_url
WHERE
`image_id` = in_id;
END IF;
END IF;
IF ROW_COUNT() <> 1 THEN
SET out_status = 'NoChange';
ELSE
SET out_status = 'Updated';
END IF;
CALL Image_StorePullHistory(in_id, in_pulls, out_status);
SELECT * FROM ImageKey_View WHERE `ImageId` = in_id;
END IF;
END //

View File

@ -1,5 +1,5 @@
#Sat Jan 18 12:56:52 GMT 2020
app.build.date=2020-01-18T12\:56\:52
#Sat Jan 18 17:28:00 GMT 2020
app.build.date=2020-01-18T17\:28\:00
app.build.os=Linux
app.build.user=josh
app.version=2.0.0

View File

@ -23,6 +23,7 @@
<#import "../../ui/form/input.ftl" as input />
<#import "../../ui/elements/button.ftl" as button />
<#import "../../ui/elements/table.ftl" as table />
<#import "../../ui/elements/tag.ftl" as tag />
<#import "../../ui/components/message.ftl" as message />
<#import "template-components/image-template-ports.ftl" as templatePorts />
@ -71,21 +72,33 @@
<label class="label" for="ImageAppLogo">App Logo</label>
</div>
<div class="field-body">
<input type="file" name="imageAppLogo" id="ImageAppLogo" />
<input type="file" name="ImageAppLogo" id="ImageAppLogo" />
</div>
</div>
</div>
<div class="column is-full">
<@input.text id="ImageBase" label="Base Image" isInline=true />
<@input.text id="ImageBase" label="Base Image" isInline=true value=image.metaData.baseImage
infoText="The name of the base image this image pulls from." />
</div>
<div class="column is-full">
<@input.text id="ImageCategory" label="Category" isInline=true />
<@input.text id="ImageCategory" label="Category" isInline=true value=image.metaData.category
infoText="The application category for this image (e.g Home Automation)." />
</div>
<div class="column is-full">
<@input.text id="ImageSupportUrl" label="Support Url" isInline=true />
<@input.text id="ImageSupportUrl" label="Support Url" isInline=true value=image.metaData.supportUrl
infoText="A link to the primary source of support for this image, such as a forum thread or documentation site." />
</div>
<div class="column is-full">
<@input.text id="ImageAppUrl" label="Application Url" isInline=true />
<@input.text id="ImageApplicationUrl" label="Application Url" isInline=true value=image.metaData.appUrl
infoText="The primary URL for the application encapsulated by this image." />
</div>
</div>
<div class="columns">
@ -132,16 +145,14 @@
<td>
<#if !tagBranch.branchProtected>
<@button.buttons isRightAligned=true>
<@button.button extraClasses="remove-tag-branch" colour="danger" size="small">
<i class="fas fa-trash"></i> Remove
<@button.button extraClasses="remove-tag-branch" colour="white" size="small" title="Stop tracking this branch.">
<i class="fas fa-trash has-text-danger is-marginless"></i>
</@button.button>
</@button.buttons>
<#else>
<@button.buttons isRightAligned=true>
<@button.button colour="light" size="small" isDisabled=true>
Protected
</@button.button>
</@button.buttons>
<div class="tags is-right">
<@tag.tag colour="light" value="Protected" extraAttributes='title="This branch can\'t be removed."' />
</div>
</#if>
</td>
</tr>

View File

@ -95,7 +95,7 @@
<div class="columns is-multiline">
<div class="column is-full">
<div class="column is-full has-margin-bottom">
<h2 class="title is-5">Build Information</h2>
<h3 class="subtitle is-6">General build information for this image</h3>
@ -109,6 +109,9 @@
<tbody>
<@table.halfDisplayRow title="Repository" value=image.repositoryName link="/?key=${image.repositoryKey}" />
<@table.halfDisplayRow title="Build Time" value=image.lastUpdatedAsString />
<#if image.metaData.baseImage?has_content>
<@table.halfDisplayRow title="Base Image" value=image.metaData.baseImage?html />
</#if>
<@table.halfDisplayRow title="Synchronised" value=image.syncEnabled?string("Yes", "No") />
<@table.halfDisplayRow title="Stable" value=image.stable?string("Yes", "No") />
<@table.halfDisplayRow title="Deprecated" value=image.deprecated?string("Yes", "No") />
@ -117,7 +120,35 @@
</div>
<div class="column is-full has-margin-bottom">
<#if image.metaData.populated>
<div class="column is-full has-margin-bottom">
<h2 class="title is-5">Support Information</h2>
<h3 class="subtitle is-6">External links and support</h3>
<@table.table isFullWidth=true isNarrow=false isStriped=true isScrollable=true>
<thead>
<tr>
<th scope="row" colspan="2"></th>
</tr>
</thead>
<tbody>
<#if image.metaData.category?has_content>
<@table.halfDisplayRow title="Category" value=image.metaData.category />
</#if>
<#if image.metaData.appUrl?has_content>
<@table.halfDisplayRow title="Application Home" value=image.metaData.appUrl?html link=image.metaData.appUrl />
</#if>
<#if image.metaData.supportUrl?has_content>
<@table.halfDisplayRow title="Support" value=image.metaData.supportUrl?html link=image.metaData.supportUrl />
</#if>
</tbody>
</@table.table>
</div>
</#if>
<div class="column is-full">
<h2 class="title is-5">Tracked Tags</h2>
<h3 class="subtitle is-6">Known tags which link to a specific branched app version.</h3>

View File

@ -22,7 +22,10 @@ version: "2"
services:
${containerName}:
image: ${fullName}<#if latest?has_content>:${latest}</#if>
container_name: ${containerName}<#if templates.restartPolicy?has_content>
container_name: ${containerName}
<#if templates.hostNetworkingEnabled> network_mode: host
</#if>
<#if templates.restartPolicy?has_content>
restart: ${templates.restartPolicy}</#if>
<#if templates.capabilities?has_content> cap_add:
<#list templates.capabilities as cap>
@ -39,7 +42,7 @@ services:
- /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro</#if> <#if volume.description?has_content># ${volume.description}</#if>
</#list>
</#if>
<#if templates.ports?has_content> ports:
<#if templates.ports?has_content && !templates.hostNetworkingEnabled> ports:
<#list templates.ports as port>
- ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol} <#if port.description?has_content># ${port.description}</#if>
</#list>
@ -57,7 +60,8 @@ services:
<div class="content">
<pre><code class="language-bash">docker create \
--name=${containerName} \<#if templates.env?has_content>
--name=${containerName} \<#if templates.hostNetworkingEnabled>
--net=host \</#if><#if templates.env?has_content>
<#list templates.env as env>
-e ${env.name}=<#if env.description?has_content> `# ${env.description}`</#if> \
</#list>
@ -67,7 +71,7 @@ services:
-v /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro</#if><#if volume.description?has_content> `# ${volume.description}`</#if> \
</#list>
</#if>
<#if templates.ports?has_content>
<#if templates.ports?has_content && !templates.hostNetworkingEnabled>
<#list templates.ports as port>
-p ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol}<#if port.description?has_content> `# ${port.description}`</#if> \
</#list>

View File

@ -15,7 +15,7 @@ Reference: https://bulma.io/documentation/form/input/
requiredHelp:String - If "required" is true, some help text to be displayed
size:enum(small, normal, large) - The size of the input
-->
<#macro input type id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal">
<#macro input type id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal">
<div class="field<#if isInline> is-horizontal</#if>">
<#if isInline>
@ -38,6 +38,9 @@ Reference: https://bulma.io/documentation/form/input/
<#if isRequired && requiredHelp?has_content>
<p class="help invalid-feedback is-danger">${requiredHelp}</p>
</#if>
<#if infoText?has_content>
<p class="help">${infoText}</p>
</#if>
</div>
<#if isInline>
</div>
@ -64,8 +67,8 @@ Convenience macro to generate a text input
requiredHelp:String - If "required" is true, some help text to be displayed
size:enum(small, normal, large) - The size of the input
-->
<#macro text id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal">
<@input type="text" id=id value=value title=title label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp size=size />
<#macro text id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal">
<@input type="text" id=id value=value title=title label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp infoText=infoText size=size />
</#macro>
<#--