724 lines
31 KiB
JavaScript
724 lines
31 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2015 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
(function() {
|
|
var angular = require('angular');
|
|
require("angular-route");
|
|
require('angular-dialog.js');
|
|
|
|
require('./kube-client');
|
|
require('./date');
|
|
require('./tags');
|
|
require('./policy');
|
|
|
|
require('registry-image-widgets/dist/image-widgets.js');
|
|
|
|
require('../views/images-page.html');
|
|
require('../views/imagestream-page.html');
|
|
require('../views/image-page.html');
|
|
require('../views/imagestream-delete.html');
|
|
require('../views/imagestream-modify.html');
|
|
require('../views/imagestream-modify.html');
|
|
require('../views/image-delete.html');
|
|
|
|
/*
|
|
* Executes callback for each stream.status.tag[x].item[y]
|
|
* in a stream. Similar behavior to angular.forEach()
|
|
*/
|
|
function imagestreamEachTagItem(stream, callback, context) {
|
|
var i, il, items;
|
|
var t, tl;
|
|
var tags = (stream.status || {}).tags || [];
|
|
for (t = 0, tl = tags.length; t < tl; t++) {
|
|
items = (tags[t].items) || [];
|
|
for (i = 0, il = items.length; i < il; i++)
|
|
callback.call(context || null, tags[t], items[i]);
|
|
}
|
|
}
|
|
|
|
function identifier(imagestream, tag) {
|
|
var id = imagestream.metadata.namespace + "/" + imagestream.metadata.name;
|
|
if (tag)
|
|
id += ":" + tag.name;
|
|
return id;
|
|
}
|
|
|
|
angular.module('registry.images', [
|
|
'ngRoute',
|
|
'ui.cockpit',
|
|
'kubeClient',
|
|
'kubernetes.date',
|
|
'registry.tags',
|
|
'registryUI.images',
|
|
])
|
|
|
|
.config([
|
|
'$routeProvider',
|
|
function($routeProvider) {
|
|
$routeProvider
|
|
.when('/images/:namespace?', {
|
|
templateUrl: 'views/images-page.html',
|
|
controller: 'ImagesCtrl'
|
|
})
|
|
.when('/images/:namespace/:target', {
|
|
controller: 'ImageCtrl',
|
|
templateUrl: function(params) {
|
|
var target = params['target'] || '';
|
|
if (target.indexOf(':') === -1)
|
|
return 'views/imagestream-page.html';
|
|
else
|
|
return 'views/image-page.html';
|
|
}
|
|
});
|
|
}
|
|
])
|
|
|
|
.factory('registryListingScopeSetup', [
|
|
'imageData',
|
|
'imageActions',
|
|
'projectData',
|
|
'kubeSelect',
|
|
'$location',
|
|
function (data, actions, projectData, select, $location) {
|
|
return function($scope, inPage) {
|
|
function imageByTag (tag) {
|
|
if (tag && tag.items && tag.items.length)
|
|
return select().kind("Image")
|
|
.name(tag.items[0].image)
|
|
.one();
|
|
}
|
|
|
|
function deleteImageStream(stream) {
|
|
var promise = actions.deleteImageStream(stream);
|
|
|
|
/* If the promise is successful, redirect to another page */
|
|
promise.then(function() {
|
|
$location.path($scope.viewUrl('images'));
|
|
});
|
|
|
|
return promise;
|
|
}
|
|
|
|
function deleteTag(stream, tag) {
|
|
var promise = actions.deleteTag(stream, tag);
|
|
|
|
/* If the promise is successful, redirect to another page */
|
|
promise.then(function() {
|
|
var parts = [ "images", stream.metadata.namespace, stream.metadata.name ];
|
|
$location.path("/" + parts.map(encodeURIComponent).join("/"));
|
|
});
|
|
|
|
return promise;
|
|
}
|
|
|
|
/* All the actions available on the $scope */
|
|
angular.extend($scope, actions);
|
|
angular.extend($scope, data);
|
|
|
|
$scope.sharedImages = projectData.sharedImages;
|
|
$scope.imageTagNames = data.imageTagNames;
|
|
$scope.imageByTag = imageByTag;
|
|
|
|
if (inPage) {
|
|
$scope.deleteTag = deleteTag;
|
|
$scope.deleteImageStream = deleteImageStream;
|
|
}
|
|
|
|
$scope.actions = {
|
|
modifyImageStream: $scope.modifyImageStream,
|
|
deleteImageStream: $scope.deleteImageStream,
|
|
deleteTag: $scope.deleteTag,
|
|
modifyProject: $scope.modifyProject,
|
|
};
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller('ImagesCtrl', [
|
|
'$scope',
|
|
'$location',
|
|
'imageData',
|
|
'imageActions',
|
|
'projectData',
|
|
'kubeLoader',
|
|
'registryListingScopeSetup',
|
|
'filterService',
|
|
function($scope, $location, data, actions, projectData, loader, registryListingScopeSetup) {
|
|
$scope.sharedImages = projectData.sharedImages;
|
|
|
|
/* Watch all the images in current namespace */
|
|
data.watchImages($scope);
|
|
|
|
$scope.imagestreams = data.allStreams();
|
|
loader.listen(function() {
|
|
$scope.imagestreams = data.allStreams();
|
|
}, $scope);
|
|
|
|
$scope.$on("activate", function(ev, imagestream, tag) {
|
|
ev.preventDefault();
|
|
$location.path('/images/' + identifier(imagestream, tag));
|
|
});
|
|
|
|
registryListingScopeSetup($scope, false);
|
|
}
|
|
])
|
|
|
|
/*
|
|
* Note that we use the same controller for both the ImageStream
|
|
* and the Image view. This is because ngRoute can't special case
|
|
* routes based on the colon we use to differentiate the two in
|
|
* the path.
|
|
*
|
|
* ie: cockpit/ws vs. cockpit/ws:latest
|
|
*
|
|
* The |kind| on the scope tells us which is which.
|
|
*/
|
|
.controller('ImageCtrl', [
|
|
'$scope',
|
|
'$location',
|
|
'$routeParams',
|
|
'kubeSelect',
|
|
'kubeLoader',
|
|
'KubeDiscoverSettings',
|
|
'imageData',
|
|
'imageActions',
|
|
'projectData',
|
|
'projectPolicy',
|
|
'registryListingScopeSetup',
|
|
function($scope, $location, $routeParams, select, loader, discoverSettings, data, actions, projectData, projectPolicy, registryListingScopeSetup) {
|
|
var target = $routeParams["target"] || "";
|
|
var pos = target.indexOf(":");
|
|
|
|
/* colon contains a tag name, only set if we're looking at an image */
|
|
var namespace = $routeParams["namespace"] || "";
|
|
var name, tagname;
|
|
if (pos === -1) {
|
|
$scope.kind = "ImageStream";
|
|
name = target;
|
|
tagname = null;
|
|
} else {
|
|
$scope.kind = "Image";
|
|
name = target.substr(0, pos);
|
|
tagname = target.substr(pos + 1);
|
|
}
|
|
|
|
registryListingScopeSetup($scope, true);
|
|
|
|
/* There's no way to watch a single item ... so watch them all :( */
|
|
data.watchImages($scope);
|
|
|
|
loader.listen(function() {
|
|
$scope.stream = select().kind("ImageStream")
|
|
.namespace(namespace)
|
|
.name(name)
|
|
.one();
|
|
$scope.image = $scope.config = $scope.layers = $scope.labels = $scope.tag = null;
|
|
|
|
imagestreamEachTagItem($scope.stream || {}, function(tag, item) {
|
|
if (tag.tag === tagname)
|
|
$scope.tag = tag;
|
|
});
|
|
|
|
if ($scope.tag)
|
|
$scope.image = $scope.imageByTag($scope.tag);
|
|
if ($scope.image) {
|
|
$scope.names = data.imageTagNames($scope.image);
|
|
$scope.config = data.imageConfig($scope.image);
|
|
$scope.layers = data.imageLayers($scope.image);
|
|
$scope.labels = data.imageLabels($scope.image);
|
|
}
|
|
}, $scope);
|
|
|
|
$scope.$on("activate", function(ev, imagestream, tag) {
|
|
ev.preventDefault();
|
|
$location.path('/images/' + identifier(imagestream, tag));
|
|
});
|
|
|
|
function updateShowDockerPushCommands() {
|
|
discoverSettings().then(function(settings) {
|
|
projectPolicy.subjectAccessReview(namespace, settings.currentUser, 'update', 'imagestreamimages')
|
|
.then(function(allowed) {
|
|
if (allowed != $scope.showDockerPushCommands) {
|
|
$scope.showDockerPushCommands = allowed;
|
|
$scope.$applyAsync();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// watch for project changes to update showDockerPushCommands, and initialize it
|
|
$scope.$on("$routeUpdate", updateShowDockerPushCommands);
|
|
updateShowDockerPushCommands();
|
|
}
|
|
])
|
|
|
|
.factory("imageData", [
|
|
'kubeSelect',
|
|
'kubeLoader',
|
|
function(select, loader) {
|
|
var watching = false;
|
|
|
|
/* Called when we have to load images via imagestreams */
|
|
loader.listen(function(objects) {
|
|
for (var link in objects) {
|
|
if (objects[link].kind === "ImageStream")
|
|
handle_imagestream(objects[link]);
|
|
if (objects[link].kind === "Image")
|
|
handle_image(objects[link]);
|
|
}
|
|
});
|
|
|
|
function handle_imagestream(imagestream) {
|
|
var meta = imagestream.metadata || { };
|
|
var status = imagestream.status || { };
|
|
angular.forEach(status.tags || [ ], function(tag) {
|
|
angular.forEach(tag.items || [ ], function(item) {
|
|
var link = loader.resolve("Image", item.image);
|
|
if (link in loader.objects)
|
|
return;
|
|
|
|
/* An interim object while we're loading */
|
|
var interim = { kind: "Image", apiVersion: "v1", metadata: { name: item.image } };
|
|
loader.handle(interim);
|
|
|
|
if (!watching)
|
|
return;
|
|
|
|
var name = meta.name + "@" + item.image;
|
|
loader.load("ImageStreamImage", name, meta.namespace).then(function(resource) {
|
|
var image = resource.image;
|
|
if (image) {
|
|
image.kind = "Image";
|
|
loader.handle(image);
|
|
handle_image(image);
|
|
}
|
|
}, function(response) {
|
|
var message = response.statusText || response.message || String(response);
|
|
console.warn("couldn't load image: " + message);
|
|
interim.metadata.resourceVersion = "invalid";
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Create a pseudo-item with kind DockerImageManifest for
|
|
* each image with a dockerImageManifest that we see. Identical
|
|
* name to the image itself.
|
|
*/
|
|
function handle_image(image) {
|
|
var item;
|
|
var manifest = image.dockerImageManifest;
|
|
if (manifest) {
|
|
manifest = JSON.parse(manifest);
|
|
angular.forEach(manifest.history || [], function(item) {
|
|
if (typeof item.v1Compatibility == "string")
|
|
item.v1Compatibility = JSON.parse(item.v1Compatibility);
|
|
});
|
|
item = {
|
|
kind: "DockerImageManifest",
|
|
metadata: {
|
|
name: image.metadata.name,
|
|
selfLink: "/internal/manifests/" + image.metadata.name
|
|
},
|
|
manifest: manifest,
|
|
};
|
|
loader.handle(item);
|
|
}
|
|
}
|
|
|
|
/* Load images, but fallback to loading individually */
|
|
function watchImages(until) {
|
|
watching = true;
|
|
var a = loader.watch("images", until);
|
|
var b = loader.watch("imagestreams", until);
|
|
|
|
return {
|
|
cancel: function() {
|
|
a.cancel();
|
|
b.cancel();
|
|
}
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Filters selection to those with names that is
|
|
* in the given TagEvent.
|
|
*/
|
|
select.register("taggedBy", function(tag) {
|
|
var i, len;
|
|
var results = { };
|
|
// catch condition when tag.items is null due to imagestream import error
|
|
if (!tag.items)
|
|
return select(null);
|
|
for (i = 0, len = tag.items.length; i < len; i++)
|
|
this.name(tag.items[i].image).extend(results);
|
|
return select(results);
|
|
});
|
|
|
|
/*
|
|
* Filters selection to those with names that is in the first
|
|
* item in the given TagEvent.
|
|
*/
|
|
select.register("taggedFirst", function(tag) {
|
|
var results = { };
|
|
if (!tag.items)
|
|
return select(null);
|
|
if (tag.items.length)
|
|
this.name(tag.items[0].image).extend(results);
|
|
return select(results);
|
|
});
|
|
|
|
/*
|
|
* Filter that gets image streams for the given tag.
|
|
*/
|
|
select.register({
|
|
name: "containsTagImage",
|
|
digests: function(arg) {
|
|
var ret = [];
|
|
if (typeof arg == "string") {
|
|
ret.push(arg);
|
|
} else {
|
|
imagestreamEachTagItem(arg, function(tag, item) {
|
|
ret.push(item.image + "");
|
|
});
|
|
}
|
|
return ret;
|
|
}
|
|
});
|
|
|
|
select.register("listTagNames", function(image_name) {
|
|
var names = [];
|
|
angular.forEach(this.containsTagImage(image_name), function(stream) {
|
|
imagestreamEachTagItem(stream, function(tag, item) {
|
|
if (!image_name || item.image == image_name)
|
|
names.push(stream.metadata.namespace + "/" + stream.metadata.name + ":" + tag.tag);
|
|
});
|
|
});
|
|
return names;
|
|
});
|
|
|
|
/*
|
|
* Filter that gets the config object for a docker based
|
|
* image.
|
|
*/
|
|
select.register("dockerImageConfig", function() {
|
|
var results = { };
|
|
angular.forEach(this, function(image, key) {
|
|
var compat;
|
|
var layers = imageLayers(image) || { };
|
|
if (layers[0]) {
|
|
compat = layers[0].v1Compatibility;
|
|
if (compat && compat.config) {
|
|
results[key] = compat.config;
|
|
return;
|
|
}
|
|
}
|
|
|
|
var meta = image.dockerImageMetadata || { };
|
|
if (meta.Config)
|
|
results[key] = meta.Config;
|
|
});
|
|
|
|
return select(results);
|
|
});
|
|
|
|
/*
|
|
* Filter that gets a dict of labels for a config
|
|
* image.
|
|
*/
|
|
select.register("dockerConfigLabels", function() {
|
|
var results = { };
|
|
angular.forEach(this, function(config, key) {
|
|
var labels;
|
|
if (config)
|
|
labels = config.Labels;
|
|
if (labels)
|
|
results[key] = labels;
|
|
});
|
|
return select(results);
|
|
});
|
|
|
|
function imageLayers(image) {
|
|
if (!image)
|
|
return null;
|
|
var item = select().kind("DockerImageManifest")
|
|
.name(image.metadata.name)
|
|
.one();
|
|
if (item && item.manifest && item.manifest.schemaVersion === 1)
|
|
return item.manifest.history;
|
|
if (image.dockerImageLayers)
|
|
return image.dockerImageLayers;
|
|
return null;
|
|
}
|
|
|
|
/* HACK: We really want a metadata index here */
|
|
function configCommand(config) {
|
|
var result = [ ];
|
|
if (!config)
|
|
return "";
|
|
if (config.Entrypoint)
|
|
result.push.apply(result, config.Entrypoint);
|
|
if (config.Cmd)
|
|
result.push.apply(result, config.Cmd);
|
|
var string = result.join(" ");
|
|
if (config.User && config.User.split(":")[0] != "root")
|
|
return "$ " + string;
|
|
else
|
|
return "# " + string;
|
|
}
|
|
|
|
return {
|
|
watchImages: watchImages,
|
|
allStreams: function allStreams() {
|
|
return select().kind("ImageStream");
|
|
},
|
|
imageLayers: imageLayers,
|
|
imageConfig: function imageConfig(image) {
|
|
return select(image).dockerImageConfig()
|
|
.one() || { };
|
|
},
|
|
imageTagNames: function imageTagNames(image) {
|
|
return select().kind("ImageStream")
|
|
.listTagNames(image.metadata.name);
|
|
},
|
|
imageLabels: function imageLabels(image) {
|
|
var labels = select(image).dockerImageConfig()
|
|
.dockerConfigLabels()
|
|
.one();
|
|
if (labels && angular.equals({ }, labels))
|
|
labels = null;
|
|
return labels;
|
|
},
|
|
configCommand: configCommand,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('imageActions', [
|
|
'$modal',
|
|
'$location',
|
|
function($modal, $location) {
|
|
function deleteImageStream(stream) {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'ImageStreamDeleteCtrl',
|
|
templateUrl: 'views/imagestream-delete.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { stream: stream };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
function createImageStream() {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'ImageStreamModifyCtrl',
|
|
templateUrl: 'views/imagestream-modify.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
function modifyImageStream(stream) {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'ImageStreamModifyCtrl',
|
|
templateUrl: 'views/imagestream-modify.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { stream: stream };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
function deleteTag(stream, tag) {
|
|
var modal = $modal.open({
|
|
animation: false,
|
|
controller: 'ImageDeleteCtrl',
|
|
templateUrl: 'views/image-delete.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { stream: stream, tag: tag };
|
|
}
|
|
},
|
|
});
|
|
|
|
return modal.result;
|
|
}
|
|
|
|
function modifyProject(project) {
|
|
$location.path("/projects/" + project);
|
|
return false;
|
|
}
|
|
|
|
return {
|
|
createImageStream: createImageStream,
|
|
modifyImageStream: modifyImageStream,
|
|
deleteImageStream: deleteImageStream,
|
|
deleteTag: deleteTag,
|
|
modifyProject: modifyProject,
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("ImageStreamDeleteCtrl", [
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"kubeMethods",
|
|
function($scope, $instance, dialogData, methods) {
|
|
angular.extend($scope, dialogData);
|
|
|
|
$scope.performDelete = function performDelete() {
|
|
return methods.delete($scope.stream);
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("ImageStreamModifyCtrl", [
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"imageTagData",
|
|
"kubeMethods",
|
|
"filterService",
|
|
"gettextCatalog",
|
|
function($scope, $instance, dialogData, tagData, methods, filter, gettextCatalog) {
|
|
var stream = dialogData.stream || { };
|
|
var meta = stream.metadata || { };
|
|
var spec = stream.spec || { };
|
|
const _ = gettextCatalog.getString.bind(gettextCatalog);
|
|
|
|
var populate = "none";
|
|
if (spec.dockerImageRepository)
|
|
populate = "pull";
|
|
if (spec.tags)
|
|
populate = "tags";
|
|
|
|
var fields = {
|
|
name: meta.name || "",
|
|
project: meta.namespace || filter.namespace() || "",
|
|
populate: populate,
|
|
pull: spec.dockerImageRepository || "",
|
|
tags: tagData.parseSpec(spec),
|
|
insecure: hasInsecureTag(spec),
|
|
};
|
|
|
|
$scope.fields = fields;
|
|
$scope.labels = {
|
|
populate: {
|
|
none: _("Don't pull images automatically"),
|
|
pull: _("Sync all tags from a remote image repository"),
|
|
tags: _("Pull specific tags from another image repository"),
|
|
}
|
|
};
|
|
|
|
$scope.placeholder = _("eg: my-image-stream");
|
|
|
|
/* During creation we have a different label */
|
|
if (!dialogData.stream)
|
|
$scope.labels.populate.none = _("Create empty image stream");
|
|
|
|
function performModify() {
|
|
var data = {
|
|
apiVersion: "v1",
|
|
kind: "ImageStream",
|
|
metadata: { annotations: { "openshift.io/image.dockerRepositoryCheck" : null } }
|
|
};
|
|
|
|
if (fields.populate == "pull")
|
|
data.spec = { dockerImageRepository: fields.pull.trim(), };
|
|
else if (fields.populate == "tags")
|
|
data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());
|
|
else
|
|
data.spec = { dockerImageRepository: null, tags: null };
|
|
|
|
return methods.patch(stream, data);
|
|
}
|
|
|
|
function performCreate() {
|
|
var data = {
|
|
apiVersion: "v1",
|
|
kind: "ImageStream",
|
|
metadata: {
|
|
name: fields.name.trim(),
|
|
namespace: fields.project.trim(),
|
|
}
|
|
};
|
|
|
|
if (fields.populate == "pull")
|
|
data.spec = { dockerImageRepository: fields.pull.trim(), };
|
|
else if (fields.populate == "tags")
|
|
data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());
|
|
|
|
return methods.check(data, {
|
|
"metadata.name": "#imagestream-modify-name",
|
|
"metadata.namespace": "#imagestream-modify-project",
|
|
}).then(function() {
|
|
return methods.create(data, fields.project);
|
|
});
|
|
}
|
|
|
|
function hasInsecureTag(spec) {
|
|
// loop through tags, check importPolicy.insecure boolean
|
|
// if one tag is insecure the intent is the imagestream is insecure
|
|
var insecure;
|
|
if (spec) {
|
|
for (var tag in spec.tags) {
|
|
if (spec.tags[tag].importPolicy.insecure) {
|
|
insecure = spec.tags[tag].importPolicy.insecure;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return insecure;
|
|
}
|
|
|
|
$scope.performCreate = performCreate;
|
|
$scope.performModify = performModify;
|
|
$scope.hasInsecureTag = hasInsecureTag;
|
|
|
|
$scope.projects = filter.namespaces;
|
|
angular.extend($scope, dialogData);
|
|
}
|
|
])
|
|
|
|
.controller("ImageDeleteCtrl", [
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"kubeMethods",
|
|
function($scope, $instance, dialogData, methods) {
|
|
angular.extend($scope, dialogData);
|
|
|
|
$scope.performDelete = function performDelete() {
|
|
var name = $scope.stream.metadata.name + ":" + $scope.tag.tag;
|
|
return methods.delete("ImageStreamTag", name, $scope.stream.metadata.namespace);
|
|
};
|
|
}
|
|
]);
|
|
}());
|